svamp-cli 0.2.60 → 0.2.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agentCommands-8IVIOSvH.mjs → agentCommands-CcvaE6am.mjs} +2 -2
- package/dist/cli.mjs +34 -33
- package/dist/{commands-BSPSCqBa.mjs → commands-CY0X_cdt.mjs} +3 -3
- package/dist/{commands-B0kLKYmc.mjs → commands-CgirOjun.mjs} +2 -1
- package/dist/{commands-qvlNefLf.mjs → commands-CjKzQm8Q.mjs} +9 -4
- package/dist/index.mjs +2 -1
- package/dist/package-DHBXuNi1.mjs +63 -0
- package/dist/{run-Dhliie9Z.mjs → run-EPzdDXeY.mjs} +399 -26
- package/dist/{run-DmWqzmqX.mjs → run-cSiQAr8c.mjs} +2 -1
- package/dist/{serveCommands-CMSmcjsp.mjs → serveCommands-sRps4L_A.mjs} +36 -20
- package/dist/{serveManager-C9pzi-2O.mjs → serveManager-pDviHaH8.mjs} +27 -8
- package/package.json +2 -2
- package/dist/package-B7BB3yVn.mjs +0 -63
|
@@ -3,12 +3,13 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
|
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync, readdirSync } from 'fs';
|
|
4
4
|
import path__default, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
6
|
+
import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { promisify } from 'util';
|
|
9
10
|
import { randomUUID, createHash } from 'node:crypto';
|
|
10
11
|
import { join as join$1 } from 'node:path';
|
|
11
|
-
import { spawn, execSync, execFile, execFileSync } from 'node:child_process';
|
|
12
|
+
import { spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
12
13
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
13
14
|
import os, { homedir, platform } from 'node:os';
|
|
14
15
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -16,7 +17,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
|
16
17
|
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
17
18
|
import { z } from 'zod';
|
|
18
19
|
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
|
|
19
|
-
import { promisify } from 'node:util';
|
|
20
|
+
import { promisify as promisify$1 } from 'node:util';
|
|
20
21
|
|
|
21
22
|
let connectToServerFn = null;
|
|
22
23
|
async function getConnectToServer() {
|
|
@@ -72,9 +73,10 @@ const ROLE_HIERARCHY = {
|
|
|
72
73
|
admin: 2
|
|
73
74
|
};
|
|
74
75
|
function normalizeAllowedUser(input, addedBy) {
|
|
76
|
+
const role = input.role && input.role in ROLE_HIERARCHY ? input.role : "admin";
|
|
75
77
|
return {
|
|
76
|
-
email: input.email.trim(),
|
|
77
|
-
role
|
|
78
|
+
email: input.email.trim().toLowerCase(),
|
|
79
|
+
role,
|
|
78
80
|
addedAt: typeof input.addedAt === "number" ? input.addedAt : Date.now(),
|
|
79
81
|
addedBy: input.addedBy ?? addedBy
|
|
80
82
|
};
|
|
@@ -88,7 +90,8 @@ function resolveRoleLevel(sharing, userEmail) {
|
|
|
88
90
|
(u) => u.email.toLowerCase() === userEmail.toLowerCase()
|
|
89
91
|
);
|
|
90
92
|
if (sharedUser) {
|
|
91
|
-
|
|
93
|
+
const storedLevel = ROLE_HIERARCHY[sharedUser.role];
|
|
94
|
+
level = typeof storedLevel === "number" ? storedLevel : ROLE_HIERARCHY.admin;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
if (sharing.publicAccess) {
|
|
@@ -302,6 +305,98 @@ function applySecurityContext(baseConfig, context) {
|
|
|
302
305
|
return config;
|
|
303
306
|
}
|
|
304
307
|
|
|
308
|
+
const execFileAsync$1 = promisify(execFile);
|
|
309
|
+
function parseEtime(s) {
|
|
310
|
+
if (!s) return 0;
|
|
311
|
+
const dayParts = s.split("-");
|
|
312
|
+
let dayPrefix = 0;
|
|
313
|
+
let rest = s;
|
|
314
|
+
if (dayParts.length === 2) {
|
|
315
|
+
dayPrefix = parseInt(dayParts[0], 10) || 0;
|
|
316
|
+
rest = dayParts[1];
|
|
317
|
+
}
|
|
318
|
+
const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
|
|
319
|
+
let total = 0;
|
|
320
|
+
if (parts.length === 3) total = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
321
|
+
else if (parts.length === 2) total = parts[0] * 60 + parts[1];
|
|
322
|
+
else if (parts.length === 1) total = parts[0];
|
|
323
|
+
return dayPrefix * 86400 + total;
|
|
324
|
+
}
|
|
325
|
+
async function readProcessTable() {
|
|
326
|
+
if (process.platform === "win32") return [];
|
|
327
|
+
let stdout;
|
|
328
|
+
try {
|
|
329
|
+
const result = await execFileAsync$1("ps", ["-A", "-o", "pid=,ppid=,etime=,%cpu=,rss=,command="], {
|
|
330
|
+
maxBuffer: 8 * 1024 * 1024
|
|
331
|
+
// 8MB — plenty for any realistic process table
|
|
332
|
+
});
|
|
333
|
+
stdout = result.stdout;
|
|
334
|
+
} catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const rows = [];
|
|
338
|
+
for (const line of stdout.split("\n")) {
|
|
339
|
+
const trimmed = line.trim();
|
|
340
|
+
if (!trimmed) continue;
|
|
341
|
+
const parts = trimmed.split(/\s+/);
|
|
342
|
+
if (parts.length < 6) continue;
|
|
343
|
+
const pid = parseInt(parts[0], 10);
|
|
344
|
+
const ppid = parseInt(parts[1], 10);
|
|
345
|
+
if (!pid || isNaN(ppid)) continue;
|
|
346
|
+
const etimeSec = parseEtime(parts[2]);
|
|
347
|
+
const cpu = parseFloat(parts[3]) || 0;
|
|
348
|
+
const rssKb = parseInt(parts[4], 10) || 0;
|
|
349
|
+
const command = parts.slice(5).join(" ");
|
|
350
|
+
rows.push({ pid, ppid, etimeSec, cpu, rssKb, command });
|
|
351
|
+
}
|
|
352
|
+
return rows;
|
|
353
|
+
}
|
|
354
|
+
async function detectDescendants(rootPid, excludePids = /* @__PURE__ */ new Set()) {
|
|
355
|
+
if (!rootPid || rootPid < 1) return [];
|
|
356
|
+
const table = await readProcessTable();
|
|
357
|
+
if (table.length === 0) return [];
|
|
358
|
+
const byPpid = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const row of table) {
|
|
360
|
+
const list = byPpid.get(row.ppid);
|
|
361
|
+
if (list) list.push(row);
|
|
362
|
+
else byPpid.set(row.ppid, [row]);
|
|
363
|
+
}
|
|
364
|
+
const descendants = [];
|
|
365
|
+
const visited = /* @__PURE__ */ new Set([rootPid]);
|
|
366
|
+
const queue = [rootPid];
|
|
367
|
+
const MAX_NODES = 5e3;
|
|
368
|
+
while (queue.length > 0 && descendants.length < MAX_NODES) {
|
|
369
|
+
const next = queue.shift();
|
|
370
|
+
const children = byPpid.get(next);
|
|
371
|
+
if (!children) continue;
|
|
372
|
+
for (const child of children) {
|
|
373
|
+
if (visited.has(child.pid)) continue;
|
|
374
|
+
visited.add(child.pid);
|
|
375
|
+
queue.push(child.pid);
|
|
376
|
+
if (!excludePids.has(child.pid)) {
|
|
377
|
+
descendants.push(child);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
descendants.sort((a, b) => a.etimeSec - b.etimeSec);
|
|
382
|
+
return descendants;
|
|
383
|
+
}
|
|
384
|
+
async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
|
|
385
|
+
if (!rootPid || rootPid < 1) return { killed: false, reason: "no root pid" };
|
|
386
|
+
if (!targetPid || targetPid < 1) return { killed: false, reason: "invalid target pid" };
|
|
387
|
+
if (targetPid === rootPid) return { killed: false, reason: "cannot kill session root pid" };
|
|
388
|
+
const descendants = await detectDescendants(rootPid);
|
|
389
|
+
if (!descendants.some((d) => d.pid === targetPid)) {
|
|
390
|
+
return { killed: false, reason: "pid is not a descendant of this session" };
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
process.kill(targetPid, signal);
|
|
394
|
+
return { killed: true };
|
|
395
|
+
} catch (err) {
|
|
396
|
+
return { killed: false, reason: err?.message ?? "kill failed" };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
305
400
|
function getParamNames(fn) {
|
|
306
401
|
const src = fn.toString();
|
|
307
402
|
const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
@@ -375,6 +470,20 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
375
470
|
}
|
|
376
471
|
}
|
|
377
472
|
};
|
|
473
|
+
const ROLE_RANK = { view: 1, interact: 2, admin: 3 };
|
|
474
|
+
const authorizeSessionAccess = async (sessionId, requiredRole, context) => {
|
|
475
|
+
try {
|
|
476
|
+
authorizeRequest(context, currentMetadata.sharing, requiredRole);
|
|
477
|
+
return;
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
const rpc = handlers.getSessionRPCHandlers?.(sessionId);
|
|
481
|
+
if (!rpc) throw new Error(`Session '${sessionId}' not found`);
|
|
482
|
+
const role = await rpc.getEffectiveRole(context).catch(() => null);
|
|
483
|
+
if (!role || (ROLE_RANK[role] ?? 0) < ROLE_RANK[requiredRole]) {
|
|
484
|
+
throw new Error(`Access denied: ${requiredRole} role required on session ${sessionId}`);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
378
487
|
const notifyListeners = (update) => {
|
|
379
488
|
const snapshot = [...listeners];
|
|
380
489
|
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
@@ -737,10 +846,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
737
846
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
738
847
|
return { sharing: currentMetadata.sharing || null };
|
|
739
848
|
},
|
|
849
|
+
/** Returns the caller's effective role on this machine (null if no access). */
|
|
850
|
+
getEffectiveRole: async (context) => {
|
|
851
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
852
|
+
const role = getEffectiveRole(context, currentMetadata.sharing);
|
|
853
|
+
return { role };
|
|
854
|
+
},
|
|
740
855
|
updateSharing: async (newSharing, context) => {
|
|
741
856
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
742
|
-
|
|
743
|
-
|
|
857
|
+
const currentOwner = currentMetadata.sharing?.owner;
|
|
858
|
+
const callerEmail = context?.user?.email;
|
|
859
|
+
const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
|
|
860
|
+
if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
|
|
861
|
+
throw new Error("Only the machine owner can transfer ownership");
|
|
862
|
+
}
|
|
863
|
+
if (currentOwner && !newSharing.owner) {
|
|
864
|
+
newSharing = { ...newSharing, owner: currentOwner };
|
|
744
865
|
}
|
|
745
866
|
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
746
867
|
newSharing = { ...newSharing, owner: context.user.email };
|
|
@@ -1096,6 +1217,126 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1096
1217
|
if (!handlers.supervisor) return [];
|
|
1097
1218
|
return handlers.supervisor.getLogs(params.idOrName, params.last ?? 50);
|
|
1098
1219
|
},
|
|
1220
|
+
// ── Session-scoped Tasks panel ───────────────────────────────────
|
|
1221
|
+
// Frontend-facing views that filter by sessionId and also walk the
|
|
1222
|
+
// OS process tree to surface unsupervised child processes (e.g.
|
|
1223
|
+
// `node server.js &` spawned from a bash tool call). These are
|
|
1224
|
+
// separate from the global `process*` RPCs above so that a shared
|
|
1225
|
+
// user can manage their own session's tasks without machine-level
|
|
1226
|
+
// access.
|
|
1227
|
+
/**
|
|
1228
|
+
* Snapshot of everything the session's Tasks panel needs to render:
|
|
1229
|
+
* - supervised: ProcessSpec/state filtered to this session
|
|
1230
|
+
* - detected: descendants of the agent PID that aren't supervised
|
|
1231
|
+
*/
|
|
1232
|
+
sessionProcessList: async (params, context) => {
|
|
1233
|
+
await authorizeSessionAccess(params.sessionId, "view", context);
|
|
1234
|
+
const supervisor = handlers.supervisor;
|
|
1235
|
+
const all = supervisor ? supervisor.list() : [];
|
|
1236
|
+
const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
|
|
1237
|
+
const agentPid = handlers.getSessionPid?.(params.sessionId);
|
|
1238
|
+
let detected = [];
|
|
1239
|
+
if (agentPid) {
|
|
1240
|
+
const supervisedPids = /* @__PURE__ */ new Set();
|
|
1241
|
+
for (const info of all) {
|
|
1242
|
+
if (info.state.pid) supervisedPids.add(info.state.pid);
|
|
1243
|
+
}
|
|
1244
|
+
detected = await detectDescendants(agentPid, supervisedPids);
|
|
1245
|
+
}
|
|
1246
|
+
return { supervised, detected, agentPid };
|
|
1247
|
+
},
|
|
1248
|
+
/**
|
|
1249
|
+
* Cheap counts for the input-bar badge — avoids the cost of a full
|
|
1250
|
+
* detection walk when the panel is closed. Counts are still
|
|
1251
|
+
* accurate enough to indicate "is anything running for this session".
|
|
1252
|
+
*/
|
|
1253
|
+
sessionProcessCounts: async (params, context) => {
|
|
1254
|
+
await authorizeSessionAccess(params.sessionId, "view", context);
|
|
1255
|
+
const supervisor = handlers.supervisor;
|
|
1256
|
+
const all = supervisor ? supervisor.list() : [];
|
|
1257
|
+
const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
|
|
1258
|
+
const supervisedRunning = supervised.filter((p) => p.state.status === "running" || p.state.status === "starting").length;
|
|
1259
|
+
const agentPid = handlers.getSessionPid?.(params.sessionId);
|
|
1260
|
+
let detected = 0;
|
|
1261
|
+
if (agentPid) {
|
|
1262
|
+
const supervisedPids = /* @__PURE__ */ new Set();
|
|
1263
|
+
for (const info of all) if (info.state.pid) supervisedPids.add(info.state.pid);
|
|
1264
|
+
const list = await detectDescendants(agentPid, supervisedPids);
|
|
1265
|
+
detected = list.length;
|
|
1266
|
+
}
|
|
1267
|
+
return { supervised: supervised.length, supervisedRunning, detected, agentPid };
|
|
1268
|
+
},
|
|
1269
|
+
/** Logs for a supervised process, gated by session ownership. */
|
|
1270
|
+
sessionProcessLogs: async (params, context) => {
|
|
1271
|
+
await authorizeSessionAccess(params.sessionId, "view", context);
|
|
1272
|
+
if (!handlers.supervisor) return [];
|
|
1273
|
+
const info = handlers.supervisor.get(params.idOrName);
|
|
1274
|
+
if (!info || info.spec.sessionId !== params.sessionId) return [];
|
|
1275
|
+
return handlers.supervisor.getLogs(params.idOrName, params.last ?? 100);
|
|
1276
|
+
},
|
|
1277
|
+
/**
|
|
1278
|
+
* Lifecycle actions on a session-bound supervised process.
|
|
1279
|
+
* `remove` requires admin; start/stop/restart require interact.
|
|
1280
|
+
*/
|
|
1281
|
+
sessionProcessAction: async (params, context) => {
|
|
1282
|
+
const requiredRole = params.action === "remove" ? "admin" : "interact";
|
|
1283
|
+
await authorizeSessionAccess(params.sessionId, requiredRole, context);
|
|
1284
|
+
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
1285
|
+
const info = handlers.supervisor.get(params.idOrName);
|
|
1286
|
+
if (!info) throw new Error(`Process '${params.idOrName}' not found`);
|
|
1287
|
+
if (info.spec.sessionId !== params.sessionId) {
|
|
1288
|
+
throw new Error(`Process '${params.idOrName}' does not belong to this session`);
|
|
1289
|
+
}
|
|
1290
|
+
switch (params.action) {
|
|
1291
|
+
case "start":
|
|
1292
|
+
return handlers.supervisor.start(params.idOrName);
|
|
1293
|
+
case "stop":
|
|
1294
|
+
return handlers.supervisor.stop(params.idOrName);
|
|
1295
|
+
case "restart":
|
|
1296
|
+
return handlers.supervisor.restart(params.idOrName);
|
|
1297
|
+
case "remove":
|
|
1298
|
+
return handlers.supervisor.remove(params.idOrName);
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
/**
|
|
1302
|
+
* Kill an unsupervised descendant of the agent. Verified by the
|
|
1303
|
+
* detection helper so callers can only target PIDs that are
|
|
1304
|
+
* actually children of this session.
|
|
1305
|
+
*/
|
|
1306
|
+
sessionProcessKill: async (params, context) => {
|
|
1307
|
+
await authorizeSessionAccess(params.sessionId, "admin", context);
|
|
1308
|
+
const agentPid = handlers.getSessionPid?.(params.sessionId);
|
|
1309
|
+
if (!agentPid) return { killed: false, reason: "no agent pid for session" };
|
|
1310
|
+
return killDescendant(agentPid, Number(params.pid), params.signal ?? "SIGTERM");
|
|
1311
|
+
},
|
|
1312
|
+
/**
|
|
1313
|
+
* Promote a detected child into supervision. Kills the original
|
|
1314
|
+
* PID and re-spawns the same command under the supervisor, so the
|
|
1315
|
+
* new process is properly tracked (PID re-parenting isn't
|
|
1316
|
+
* achievable without ptrace).
|
|
1317
|
+
*/
|
|
1318
|
+
sessionProcessAdopt: async (params, context) => {
|
|
1319
|
+
await authorizeSessionAccess(params.sessionId, "admin", context);
|
|
1320
|
+
if (!handlers.supervisor) throw new Error("Process supervisor not available");
|
|
1321
|
+
const agentPid = handlers.getSessionPid?.(params.sessionId);
|
|
1322
|
+
if (!agentPid) throw new Error("No agent pid for session");
|
|
1323
|
+
if (params.pid && params.pid > 0) {
|
|
1324
|
+
const res = await killDescendant(agentPid, Number(params.pid), "SIGTERM");
|
|
1325
|
+
if (!res.killed && res.reason !== "pid is not a descendant of this session") ;
|
|
1326
|
+
}
|
|
1327
|
+
const spec = {
|
|
1328
|
+
name: params.name,
|
|
1329
|
+
command: params.command,
|
|
1330
|
+
args: params.args ?? [],
|
|
1331
|
+
workdir: params.workdir ?? process.cwd(),
|
|
1332
|
+
keepAlive: params.keepAlive ?? false,
|
|
1333
|
+
maxRestarts: 0,
|
|
1334
|
+
restartDelay: 2,
|
|
1335
|
+
sessionId: params.sessionId
|
|
1336
|
+
};
|
|
1337
|
+
const result = await handlers.supervisor.apply(spec);
|
|
1338
|
+
return result.info;
|
|
1339
|
+
},
|
|
1099
1340
|
// ── Service / Tunnel management ──────────────────────────────────
|
|
1100
1341
|
/** List active tunnels (replaces the old agent-sandbox serviceList). */
|
|
1101
1342
|
serviceList: async (context) => {
|
|
@@ -1804,8 +2045,14 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1804
2045
|
},
|
|
1805
2046
|
updateSharing: async (newSharing, context) => {
|
|
1806
2047
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
1807
|
-
|
|
1808
|
-
|
|
2048
|
+
const currentOwner = metadata.sharing?.owner;
|
|
2049
|
+
const callerEmail = context?.user?.email;
|
|
2050
|
+
const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
|
|
2051
|
+
if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
|
|
2052
|
+
throw new Error("Only the session owner can transfer ownership");
|
|
2053
|
+
}
|
|
2054
|
+
if (currentOwner && !newSharing.owner) {
|
|
2055
|
+
newSharing = { ...newSharing, owner: currentOwner };
|
|
1809
2056
|
}
|
|
1810
2057
|
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1811
2058
|
newSharing = { ...newSharing, owner: context.user.email };
|
|
@@ -1828,12 +2075,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1828
2075
|
return { success: true, sharing: newSharing };
|
|
1829
2076
|
},
|
|
1830
2077
|
/** Update security context and restart the agent process with new rules.
|
|
1831
|
-
* Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely.
|
|
2078
|
+
* Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely.
|
|
2079
|
+
* Admin tier (owner or admin-shared user) can perform this. */
|
|
1832
2080
|
updateSecurityContext: async (newSecurityContext, context) => {
|
|
1833
2081
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
1834
|
-
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1835
|
-
throw new Error("Only the session owner can update security context");
|
|
1836
|
-
}
|
|
1837
2082
|
if (!callbacks.onUpdateSecurityContext) {
|
|
1838
2083
|
throw new Error("Security context updates are not supported for this session");
|
|
1839
2084
|
}
|
|
@@ -4655,7 +4900,7 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
4655
4900
|
GeminiTransport: GeminiTransport
|
|
4656
4901
|
});
|
|
4657
4902
|
|
|
4658
|
-
const execFileAsync = promisify(execFile);
|
|
4903
|
+
const execFileAsync = promisify$1(execFile$1);
|
|
4659
4904
|
const SVAMP_TOOLS_DIR = join$1(homedir(), ".svamp", "tools");
|
|
4660
4905
|
const SVAMP_BIN_DIR = join$1(SVAMP_TOOLS_DIR, "bin");
|
|
4661
4906
|
async function checkCommand(command, versionArgs) {
|
|
@@ -5401,9 +5646,13 @@ class ServeAuth {
|
|
|
5401
5646
|
}
|
|
5402
5647
|
/**
|
|
5403
5648
|
* Check if a user email is authorized for a mount's access level.
|
|
5649
|
+
* Note: 'link' mounts are gated by the auth proxy at the path-segment
|
|
5650
|
+
* level (capability token), not by this method — they should never reach
|
|
5651
|
+
* here. We treat them as deny-by-default just in case.
|
|
5404
5652
|
*/
|
|
5405
5653
|
isAuthorized(email, access, ownerEmail) {
|
|
5406
5654
|
if (access === "public") return true;
|
|
5655
|
+
if (access === "link") return false;
|
|
5407
5656
|
if (!email) return false;
|
|
5408
5657
|
if (access === "owner") {
|
|
5409
5658
|
return !!ownerEmail && email.toLowerCase() === ownerEmail.toLowerCase();
|
|
@@ -6156,6 +6405,71 @@ You may be running in parallel with other agents \u2014 possibly sharing the sam
|
|
|
6156
6405
|
`;
|
|
6157
6406
|
}
|
|
6158
6407
|
|
|
6408
|
+
const STANDARD_WINDOWS = [2e5, 1e6];
|
|
6409
|
+
function readWindow(entry) {
|
|
6410
|
+
if (!entry) return 0;
|
|
6411
|
+
const cw = entry.contextWindow ?? entry.context_window;
|
|
6412
|
+
return typeof cw === "number" && cw > 0 && Number.isFinite(cw) ? cw : 0;
|
|
6413
|
+
}
|
|
6414
|
+
function computeObservedContext(usage) {
|
|
6415
|
+
if (!usage) return 0;
|
|
6416
|
+
return (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
6417
|
+
}
|
|
6418
|
+
function inferWindowFromModelName(model) {
|
|
6419
|
+
if (!model) return 0;
|
|
6420
|
+
if (/\[1m\]/i.test(model)) return 1e6;
|
|
6421
|
+
if (/opus-?4-?[7-9]/i.test(model)) return 1e6;
|
|
6422
|
+
if (/opus-?[5-9]/i.test(model)) return 1e6;
|
|
6423
|
+
if (/sonnet-?4-?[5-9]/i.test(model)) return 1e6;
|
|
6424
|
+
if (/sonnet-?[5-9]/i.test(model)) return 1e6;
|
|
6425
|
+
if (/opus|sonnet/i.test(model)) return 2e5;
|
|
6426
|
+
if (/haiku/i.test(model)) return 2e5;
|
|
6427
|
+
return 0;
|
|
6428
|
+
}
|
|
6429
|
+
function matchedMainModel(modelUsage, mainModel) {
|
|
6430
|
+
if (!modelUsage || !mainModel) return { window: 0, matched: false };
|
|
6431
|
+
const variants = [
|
|
6432
|
+
mainModel,
|
|
6433
|
+
`${mainModel}[1m]`,
|
|
6434
|
+
mainModel.replace(/\[1m\]$/i, "")
|
|
6435
|
+
];
|
|
6436
|
+
for (const k of variants) {
|
|
6437
|
+
const cw = readWindow(modelUsage[k]);
|
|
6438
|
+
if (cw > 0) return { window: cw, matched: true };
|
|
6439
|
+
}
|
|
6440
|
+
return { window: 0, matched: false };
|
|
6441
|
+
}
|
|
6442
|
+
function maxAcrossModelUsage(modelUsage) {
|
|
6443
|
+
if (!modelUsage) return 0;
|
|
6444
|
+
let best = 0;
|
|
6445
|
+
for (const v of Object.values(modelUsage)) {
|
|
6446
|
+
const cw = readWindow(v);
|
|
6447
|
+
if (cw > best) best = cw;
|
|
6448
|
+
}
|
|
6449
|
+
return best;
|
|
6450
|
+
}
|
|
6451
|
+
function selfHealForObserved(candidate, observed) {
|
|
6452
|
+
if (observed <= candidate) return candidate;
|
|
6453
|
+
const next = STANDARD_WINDOWS.find((w) => w >= observed);
|
|
6454
|
+
if (next) return Math.max(candidate, next);
|
|
6455
|
+
return Math.max(candidate, Math.ceil(observed * 1.25));
|
|
6456
|
+
}
|
|
6457
|
+
function resolveContextWindow(opts) {
|
|
6458
|
+
const { modelUsage, usage, mainModel, currentWindow } = opts;
|
|
6459
|
+
const observed = computeObservedContext(usage);
|
|
6460
|
+
const main = matchedMainModel(modelUsage, mainModel);
|
|
6461
|
+
let candidate = main.window;
|
|
6462
|
+
const definitiveMatch = main.matched;
|
|
6463
|
+
if (!candidate) candidate = maxAcrossModelUsage(modelUsage);
|
|
6464
|
+
if (!candidate) candidate = inferWindowFromModelName(mainModel);
|
|
6465
|
+
candidate = selfHealForObserved(candidate, observed);
|
|
6466
|
+
const current = typeof currentWindow === "number" && currentWindow > 0 ? currentWindow : 0;
|
|
6467
|
+
if (current > 0 && candidate > 0 && candidate < current && !definitiveMatch) {
|
|
6468
|
+
candidate = current;
|
|
6469
|
+
}
|
|
6470
|
+
return candidate;
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6159
6473
|
const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
|
|
6160
6474
|
function generateHookSettings(portOrOptions = {}) {
|
|
6161
6475
|
const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
|
|
@@ -7441,7 +7755,7 @@ async function startDaemon(options) {
|
|
|
7441
7755
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
7442
7756
|
saveExposedTunnels(list);
|
|
7443
7757
|
}
|
|
7444
|
-
const { ServeManager } = await import('./serveManager-
|
|
7758
|
+
const { ServeManager } = await import('./serveManager-pDviHaH8.mjs');
|
|
7445
7759
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
7446
7760
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
7447
7761
|
});
|
|
@@ -7645,6 +7959,7 @@ async function startDaemon(options) {
|
|
|
7645
7959
|
let lastSpawnMeta = persisted?.spawnMeta || {};
|
|
7646
7960
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
7647
7961
|
let lastAssistantText = "";
|
|
7962
|
+
let lastMainModel;
|
|
7648
7963
|
let consecutiveRalphErrors = 0;
|
|
7649
7964
|
const MAX_RALPH_ERRORS = 3;
|
|
7650
7965
|
let spawnHasReceivedInit = false;
|
|
@@ -7738,6 +8053,9 @@ async function startDaemon(options) {
|
|
|
7738
8053
|
let isKillingClaude = false;
|
|
7739
8054
|
let isRestartingClaude = false;
|
|
7740
8055
|
let isSwitchingMode = false;
|
|
8056
|
+
const OVERLOAD_BAIL_THRESHOLD = 3;
|
|
8057
|
+
let consecutiveOverloadRetries = 0;
|
|
8058
|
+
let overloadBailedThisTurn = false;
|
|
7741
8059
|
let checkSvampConfig;
|
|
7742
8060
|
let cleanupSvampConfig;
|
|
7743
8061
|
const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
@@ -7969,6 +8287,12 @@ async function startDaemon(options) {
|
|
|
7969
8287
|
"event"
|
|
7970
8288
|
);
|
|
7971
8289
|
}
|
|
8290
|
+
if (msg.type === "assistant") {
|
|
8291
|
+
const assistantModel = msg.message?.model;
|
|
8292
|
+
if (typeof assistantModel === "string" && assistantModel.length > 0) {
|
|
8293
|
+
lastMainModel = assistantModel;
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
7972
8296
|
const assistantContent = msg.type === "assistant" ? msg.message?.content ?? msg.content : void 0;
|
|
7973
8297
|
if (Array.isArray(assistantContent)) {
|
|
7974
8298
|
for (const block of assistantContent) {
|
|
@@ -7993,13 +8317,21 @@ async function startDaemon(options) {
|
|
|
7993
8317
|
logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
|
|
7994
8318
|
startupFailureRetryPending = true;
|
|
7995
8319
|
lastErrorMessagePushed = true;
|
|
8320
|
+
} else if (overloadBailedThisTurn) {
|
|
8321
|
+
logger.log(`[Session ${sessionId}] Suppressing duplicate error \u2014 overload hint already pushed for this turn`);
|
|
8322
|
+
lastErrorMessagePushed = true;
|
|
7996
8323
|
} else {
|
|
7997
8324
|
const lower = resultText.toLowerCase();
|
|
8325
|
+
const isOverload = msg.api_error_status === 529 || lower.includes("529") || lower.includes("overload");
|
|
7998
8326
|
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
7999
8327
|
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
8000
8328
|
const isBillingIssue = lower.includes("credit") || lower.includes("balance") || lower.includes("billing") || lower.includes("quota") || lower.includes("subscription") || lower.includes("payment");
|
|
8001
8329
|
let hint = "";
|
|
8002
|
-
if (
|
|
8330
|
+
if (isOverload) {
|
|
8331
|
+
const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
|
|
8332
|
+
const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
|
|
8333
|
+
hint = "\n\nAnthropic returned HTTP 529 Overloaded \u2014 server-side capacity, not your account. This session may be pinned to a hot backend shard.\n\u2022 Start a fresh session (different conversation id \u2192 different shard).\n\u2022 Wait a minute and resend.\n\u2022 Check https://status.claude.com." + proxyHint;
|
|
8334
|
+
} else if (isBillingIssue) {
|
|
8003
8335
|
hint = "\n\nCheck your Claude account credits or subscription at https://console.anthropic.com.";
|
|
8004
8336
|
} else if (isLoginIssue) {
|
|
8005
8337
|
checkAndRefreshOAuthToken(true, logger).then((r) => {
|
|
@@ -8064,15 +8396,15 @@ async function startDaemon(options) {
|
|
|
8064
8396
|
}
|
|
8065
8397
|
signalProcessing(false);
|
|
8066
8398
|
sessionWasProcessing = false;
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8399
|
+
const resolvedWindow = resolveContextWindow({
|
|
8400
|
+
modelUsage: msg.modelUsage,
|
|
8401
|
+
usage: msg.usage,
|
|
8402
|
+
mainModel: lastMainModel,
|
|
8403
|
+
currentWindow: sessionMetadata.contextWindow
|
|
8404
|
+
});
|
|
8405
|
+
if (resolvedWindow > 0 && resolvedWindow !== sessionMetadata.contextWindow) {
|
|
8406
|
+
sessionMetadata = { ...sessionMetadata, contextWindow: resolvedWindow };
|
|
8407
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
8076
8408
|
}
|
|
8077
8409
|
if (claudeResumeId && !trackedSession.stopped) {
|
|
8078
8410
|
saveSession({
|
|
@@ -8287,6 +8619,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8287
8619
|
sessionService.pushMessage(msg, "agent");
|
|
8288
8620
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
8289
8621
|
lastAssistantText = "";
|
|
8622
|
+
consecutiveOverloadRetries = 0;
|
|
8623
|
+
overloadBailedThisTurn = false;
|
|
8290
8624
|
if (!userMessagePending) {
|
|
8291
8625
|
turnInitiatedByUser = false;
|
|
8292
8626
|
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
@@ -8295,6 +8629,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8295
8629
|
if (msg.slash_commands && Array.isArray(msg.slash_commands)) {
|
|
8296
8630
|
sessionMetadata = { ...sessionMetadata, slashCommands: msg.slash_commands };
|
|
8297
8631
|
}
|
|
8632
|
+
if (typeof msg.model === "string" && msg.model.length > 0) {
|
|
8633
|
+
lastMainModel = msg.model;
|
|
8634
|
+
}
|
|
8298
8635
|
if (msg.session_id) {
|
|
8299
8636
|
const isResumeFailure = !spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
8300
8637
|
const isConversationClear = spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
@@ -8331,6 +8668,34 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8331
8668
|
sessionService.updateMetadata(sessionMetadata);
|
|
8332
8669
|
}
|
|
8333
8670
|
sessionService.pushMessage(msg, "session");
|
|
8671
|
+
} else if (msg.type === "system" && msg.subtype === "api_retry") {
|
|
8672
|
+
if (msg.error_status === 529) {
|
|
8673
|
+
consecutiveOverloadRetries++;
|
|
8674
|
+
if (consecutiveOverloadRetries >= OVERLOAD_BAIL_THRESHOLD && !overloadBailedThisTurn) {
|
|
8675
|
+
overloadBailedThisTurn = true;
|
|
8676
|
+
const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
|
|
8677
|
+
const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (which rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
|
|
8678
|
+
const hint = `Anthropic returned HTTP 529 Overloaded ${consecutiveOverloadRetries} times in a row. This is server-side capacity, not your account \u2014 typically a hot backend shard pinned to this session. Stopping early so you don't wait for the SDK's full ~4 min retry storm.
|
|
8679
|
+
|
|
8680
|
+
**Try:**
|
|
8681
|
+
\u2022 Start a fresh session (different conversation id \u2192 different shard).
|
|
8682
|
+
\u2022 Wait a minute and resend.
|
|
8683
|
+
\u2022 Check https://status.claude.com.` + proxyHint;
|
|
8684
|
+
logger.log(`[Session ${sessionId}] Bailing on ${consecutiveOverloadRetries}\xD7 consecutive 529 retries \u2014 sending interrupt`);
|
|
8685
|
+
sessionService.pushMessage({ type: "message", message: hint, level: "warning" }, "event");
|
|
8686
|
+
try {
|
|
8687
|
+
if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
|
|
8688
|
+
const interruptMsg = JSON.stringify({ type: "control_request", request: { type: "interrupt" } });
|
|
8689
|
+
claudeProcess.stdin.write(interruptMsg + "\n");
|
|
8690
|
+
}
|
|
8691
|
+
} catch (err) {
|
|
8692
|
+
logger.log(`[Session ${sessionId}] Failed to send overload interrupt: ${err.message}`);
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
} else {
|
|
8696
|
+
consecutiveOverloadRetries = 0;
|
|
8697
|
+
}
|
|
8698
|
+
sessionService.pushMessage(msg, "agent");
|
|
8334
8699
|
} else if (msg.type === "system" && msg.subtype === "task_notification" && msg.status === "completed") {
|
|
8335
8700
|
backgroundTaskCount = Math.max(0, backgroundTaskCount - 1);
|
|
8336
8701
|
if (backgroundTaskNames.length > 0) {
|
|
@@ -9820,6 +10185,14 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9820
10185
|
}
|
|
9821
10186
|
return ids;
|
|
9822
10187
|
},
|
|
10188
|
+
getSessionPid: (sessionId) => {
|
|
10189
|
+
for (const [, session] of pidToTrackedSession) {
|
|
10190
|
+
if (session.svampSessionId === sessionId && !session.stopped) {
|
|
10191
|
+
return session.pid;
|
|
10192
|
+
}
|
|
10193
|
+
}
|
|
10194
|
+
return void 0;
|
|
10195
|
+
},
|
|
9823
10196
|
supervisor,
|
|
9824
10197
|
tunnels,
|
|
9825
10198
|
serveManager,
|
|
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
5
|
-
import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-
|
|
5
|
+
import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-EPzdDXeY.mjs';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
@@ -13,6 +13,7 @@ import 'path';
|
|
|
13
13
|
import 'url';
|
|
14
14
|
import 'child_process';
|
|
15
15
|
import 'crypto';
|
|
16
|
+
import 'util';
|
|
16
17
|
import '@agentclientprotocol/sdk';
|
|
17
18
|
import '@modelcontextprotocol/sdk/client/index.js';
|
|
18
19
|
import '@modelcontextprotocol/sdk/client/stdio.js';
|