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) {
@@ -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);
@@ -3293,10 +4286,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3293
4286
  }
3294
4287
  };
3295
4288
  pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
3296
- sessionService.updateMetadata({
3297
- ...sessionMetadata,
3298
- lifecycleState: "idle"
3299
- });
4289
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
4290
+ sessionService.updateMetadata(sessionMetadata);
3300
4291
  logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
3301
4292
  return {
3302
4293
  type: "success",
@@ -3311,8 +4302,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3311
4302
  };
3312
4303
  }
3313
4304
  };
3314
- const spawnAcpSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
3315
- logger.log(`[ACP] Spawning ${agentName} session: ${sessionId}`);
4305
+ const spawnAgentSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
4306
+ logger.log(`[Agent] Spawning ${agentName} session: ${sessionId}`);
3316
4307
  try {
3317
4308
  let parseBashPermission2 = function(permission) {
3318
4309
  if (permission === "Bash") return;
@@ -3353,7 +4344,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3353
4344
  startedFromDaemon: true,
3354
4345
  startedBy: "daemon",
3355
4346
  lifecycleState: "starting",
3356
- flavor: agentName
4347
+ flavor: agentName,
4348
+ sharing: options.sharing
3357
4349
  };
3358
4350
  let currentPermissionMode = "default";
3359
4351
  const allowedTools = /* @__PURE__ */ new Set();
@@ -3367,7 +4359,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3367
4359
  { controlledByUser: false },
3368
4360
  {
3369
4361
  onUserMessage: (content, meta) => {
3370
- logger.log(`[ACP Session ${sessionId}] User message received`);
4362
+ logger.log(`[${agentName} Session ${sessionId}] User message received`);
3371
4363
  let text;
3372
4364
  let msgMeta = meta;
3373
4365
  try {
@@ -3387,17 +4379,17 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3387
4379
  if (msgMeta?.permissionMode) {
3388
4380
  currentPermissionMode = msgMeta.permissionMode;
3389
4381
  }
3390
- acpBackend.sendPrompt(sessionId, text).catch((err) => {
3391
- logger.error(`[ACP Session ${sessionId}] Error sending prompt:`, err);
4382
+ agentBackend.sendPrompt(sessionId, text).catch((err) => {
4383
+ logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
3392
4384
  });
3393
4385
  },
3394
4386
  onAbort: () => {
3395
- logger.log(`[ACP Session ${sessionId}] Abort requested`);
3396
- acpBackend.cancel(sessionId).catch(() => {
4387
+ logger.log(`[${agentName} Session ${sessionId}] Abort requested`);
4388
+ agentBackend.cancel(sessionId).catch(() => {
3397
4389
  });
3398
4390
  },
3399
4391
  onPermissionResponse: (params) => {
3400
- logger.log(`[ACP Session ${sessionId}] Permission response:`, JSON.stringify(params));
4392
+ logger.log(`[${agentName} Session ${sessionId}] Permission response:`, JSON.stringify(params));
3401
4393
  const requestId = params.id;
3402
4394
  if (params.mode) currentPermissionMode = params.mode;
3403
4395
  if (params.allowTools && Array.isArray(params.allowTools)) {
@@ -3410,17 +4402,18 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3410
4402
  }
3411
4403
  }
3412
4404
  permissionHandler.resolvePermission(requestId, params.approved);
4405
+ agentBackend.respondToPermission?.(requestId, params.approved);
3413
4406
  },
3414
4407
  onSwitchMode: (mode) => {
3415
- logger.log(`[ACP Session ${sessionId}] Switch mode: ${mode}`);
4408
+ logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
3416
4409
  currentPermissionMode = mode;
3417
4410
  },
3418
4411
  onRestartClaude: async () => {
3419
- logger.log(`[ACP Session ${sessionId}] Restart agent requested`);
4412
+ logger.log(`[${agentName} Session ${sessionId}] Restart agent requested`);
3420
4413
  return { success: false, message: "Restart is not supported for this agent type." };
3421
4414
  },
3422
4415
  onKillSession: () => {
3423
- logger.log(`[ACP Session ${sessionId}] Kill session requested`);
4416
+ logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
3424
4417
  stopSession(sessionId);
3425
4418
  },
3426
4419
  onBash: async (command, cwd) => {
@@ -3522,21 +4515,46 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3522
4515
  sessionService,
3523
4516
  logger
3524
4517
  );
3525
- const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
3526
4518
  const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
3527
- 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
- });
4519
+ let agentIsoConfig;
4520
+ if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
4521
+ const method = isolationCapabilities.preferred;
4522
+ const detail = isolationCapabilities.details[method];
4523
+ if (detail.found && detail.verified !== false) {
4524
+ agentIsoConfig = {
4525
+ method,
4526
+ binaryPath: detail.path || method,
4527
+ workspacePath: directory
4528
+ };
4529
+ sessionMetadata = { ...sessionMetadata, isolationMethod: method };
4530
+ logger.log(`[Agent Session ${sessionId}] Isolation: ${method}`);
4531
+ }
4532
+ }
4533
+ let agentBackend;
4534
+ if (KNOWN_MCP_AGENTS[agentName]) {
4535
+ agentBackend = new CodexMcpBackend({
4536
+ cwd: directory,
4537
+ env: options.environmentVariables,
4538
+ log: logger.log,
4539
+ isolationConfig: agentIsoConfig
4540
+ });
4541
+ } else {
4542
+ const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
4543
+ const acpConfig = KNOWN_ACP_AGENTS[agentName];
4544
+ agentBackend = new AcpBackend({
4545
+ agentName,
4546
+ cwd: directory,
4547
+ command: acpConfig.command,
4548
+ args: acpConfig.args,
4549
+ env: options.environmentVariables,
4550
+ permissionHandler,
4551
+ transportHandler,
4552
+ log: logger.log,
4553
+ isolationConfig: agentIsoConfig
4554
+ });
4555
+ }
3538
4556
  bridgeAcpToSession(
3539
- acpBackend,
4557
+ agentBackend,
3540
4558
  sessionService,
3541
4559
  () => sessionMetadata,
3542
4560
  (updater) => {
@@ -3555,33 +4573,28 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
3555
4573
  directory,
3556
4574
  resumeSessionId,
3557
4575
  get childProcess() {
3558
- return acpBackend.getProcess() || void 0;
4576
+ return agentBackend.getProcess?.() || void 0;
3559
4577
  }
3560
4578
  };
3561
4579
  pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
3562
- logger.log(`[ACP Session ${sessionId}] Starting ${agentName} backend...`);
3563
- acpBackend.startSession().then(() => {
3564
- logger.log(`[ACP Session ${sessionId}] ${agentName} backend started, waiting for first message`);
4580
+ logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
4581
+ agentBackend.startSession().then(() => {
4582
+ logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
3565
4583
  }).catch((err) => {
3566
- logger.error(`[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);
4584
+ logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
4585
+ sessionService.pushMessage(
4586
+ { type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure the ${agentName} CLI is installed.` },
4587
+ "event"
4588
+ );
4589
+ sessionService.sendSessionEnd();
3577
4590
  });
3578
4591
  return {
3579
4592
  type: "success",
3580
4593
  sessionId,
3581
- message: `ACP session (${agentName}) registered as svamp-session-${sessionId}`
4594
+ message: `Agent session (${agentName}) registered as svamp-session-${sessionId}`
3582
4595
  };
3583
4596
  } catch (err) {
3584
- logger.error(`[ACP] Failed to spawn ${agentName} session:`, err);
4597
+ logger.error(`[Agent] Failed to spawn ${agentName} session:`, err);
3585
4598
  return {
3586
4599
  type: "error",
3587
4600
  errorMessage: `Failed to spawn ${agentName} session: ${err.message}`
@@ -3610,6 +4623,25 @@ Please ensure the ${agentName} CLI is installed.`
3610
4623
  logger.log(`Session ${sessionId} not found`);
3611
4624
  return false;
3612
4625
  };
4626
+ const restartSession = async (sessionId) => {
4627
+ for (const session of pidToTrackedSession.values()) {
4628
+ if (session.svampSessionId === sessionId && !session.stopped) {
4629
+ if (session.restartAgent) {
4630
+ return await session.restartAgent();
4631
+ }
4632
+ return { success: false, message: "This session does not support restart." };
4633
+ }
4634
+ }
4635
+ return { success: false, message: `Session ${sessionId} not found or already stopped.` };
4636
+ };
4637
+ let isolationCapabilities;
4638
+ try {
4639
+ isolationCapabilities = await detectIsolationCapabilities();
4640
+ logger.log(`Isolation capabilities: ${isolationCapabilities.available.join(", ") || "none"} (preferred: ${isolationCapabilities.preferred || "none"})`);
4641
+ } catch (err) {
4642
+ logger.log(`Failed to detect isolation capabilities: ${err}`);
4643
+ isolationCapabilities = { available: [], preferred: null, details: { srt: { found: false }, bwrap: { found: false }, docker: { found: false }, podman: { found: false } } };
4644
+ }
3613
4645
  const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
3614
4646
  const machineMetadata = {
3615
4647
  host: os.hostname(),
@@ -3618,7 +4650,8 @@ Please ensure the ${agentName} CLI is installed.`
3618
4650
  homeDir: defaultHomeDir,
3619
4651
  svampHomeDir: SVAMP_HOME,
3620
4652
  svampLibDir: join$1(__dirname$1, ".."),
3621
- displayName: process.env.SVAMP_DISPLAY_NAME || void 0
4653
+ displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
4654
+ isolationCapabilities
3622
4655
  };
3623
4656
  const initialDaemonState = {
3624
4657
  status: "running",
@@ -3633,6 +4666,7 @@ Please ensure the ${agentName} CLI is installed.`
3633
4666
  {
3634
4667
  spawnSession,
3635
4668
  stopSession,
4669
+ restartSession,
3636
4670
  requestShutdown: () => requestShutdown("hypha-app"),
3637
4671
  getTrackedSessions: getCurrentChildren
3638
4672
  }
@@ -3727,7 +4761,7 @@ Please ensure the ${agentName} CLI is installed.`
3727
4761
  console.log(` Service: svamp-machine-${machineId}`);
3728
4762
  console.log(` Log file: ${logger.logFilePath}`);
3729
4763
  let consecutiveHeartbeatFailures = 0;
3730
- const MAX_HEARTBEAT_FAILURES = 10;
4764
+ const MAX_HEARTBEAT_FAILURES = 60;
3731
4765
  const heartbeatInterval = setInterval(async () => {
3732
4766
  try {
3733
4767
  const state = readDaemonStateFile();
@@ -3828,16 +4862,22 @@ Please ensure the ${agentName} CLI is installed.`
3828
4862
  }
3829
4863
  }
3830
4864
  }
3831
- try {
3832
- const index = loadSessionIndex();
3833
- for (const [sessionId, entry] of Object.entries(index)) {
3834
- const filePath = getSessionFilePath(entry.directory, sessionId);
3835
- if (existsSync$1(filePath)) {
3836
- const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
3837
- writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
4865
+ const shouldMarkStopped = source === "os-signal-cleanup";
4866
+ if (shouldMarkStopped) {
4867
+ try {
4868
+ const index = loadSessionIndex();
4869
+ for (const [sessionId, entry] of Object.entries(index)) {
4870
+ const filePath = getSessionFilePath(entry.directory, sessionId);
4871
+ if (existsSync$1(filePath)) {
4872
+ const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
4873
+ writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
4874
+ }
3838
4875
  }
4876
+ logger.log("Marked all sessions as stopped (--cleanup mode)");
4877
+ } catch {
3839
4878
  }
3840
- } catch {
4879
+ } else {
4880
+ logger.log("Sessions preserved for auto-restore on next start");
3841
4881
  }
3842
4882
  try {
3843
4883
  await machineService.disconnect();
@@ -3873,16 +4913,18 @@ Please ensure the ${agentName} CLI is installed.`
3873
4913
  process.exit(1);
3874
4914
  }
3875
4915
  }
3876
- async function stopDaemon() {
4916
+ async function stopDaemon(options) {
3877
4917
  const state = readDaemonStateFile();
3878
4918
  if (!state) {
3879
4919
  console.log("No daemon running");
3880
4920
  return;
3881
4921
  }
4922
+ const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
4923
+ const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
3882
4924
  try {
3883
4925
  process.kill(state.pid, 0);
3884
- process.kill(state.pid, "SIGTERM");
3885
- console.log(`Sent SIGTERM to daemon PID ${state.pid}`);
4926
+ process.kill(state.pid, signal);
4927
+ console.log(`Sent ${signal} to daemon PID ${state.pid} \u2014 ${mode}`);
3886
4928
  for (let i = 0; i < 30; i++) {
3887
4929
  await new Promise((r) => setTimeout(r, 100));
3888
4930
  try {