svamp-cli 0.1.28 → 0.1.30

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