svamp-cli 0.2.59 → 0.2.63
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-DU0bGhdt.mjs → agentCommands-BH6CIqc7.mjs} +2 -2
- package/dist/cli.mjs +34 -33
- package/dist/{commands-DXZwCUve.mjs → commands-BFeHwsR-.mjs} +9 -4
- package/dist/{commands-iYxal2yb.mjs → commands-CEazgqvj.mjs} +2 -1
- package/dist/{commands-DNpgg4Td.mjs → commands-CY-HbTVN.mjs} +3 -3
- package/dist/index.mjs +2 -1
- package/dist/{package-CYdzHadw.mjs → package-QQovcM_B.mjs} +1 -1
- package/dist/{run-COWbdmBJ.mjs → run-DB2WIjmZ.mjs} +2 -1
- package/dist/{run-FTguq4Qt.mjs → run-DypcS01S.mjs} +337 -17
- package/dist/{serveCommands-C25VGmW2.mjs → serveCommands-CtAX-CMI.mjs} +36 -20
- package/dist/{serveManager-wSTZ4YwV.mjs → serveManager-G9PiLNKm.mjs} +27 -8
- package/package.json +1 -1
|
@@ -148,7 +148,7 @@ async function sessionBroadcast(action, args) {
|
|
|
148
148
|
console.log(`Broadcast sent: ${action}`);
|
|
149
149
|
}
|
|
150
150
|
async function connectToMachineService() {
|
|
151
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
151
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
152
152
|
return connectAndGetMachine();
|
|
153
153
|
}
|
|
154
154
|
async function inboxSend(targetSessionId, opts) {
|
|
@@ -165,7 +165,7 @@ async function inboxSend(targetSessionId, opts) {
|
|
|
165
165
|
}
|
|
166
166
|
const { server, machine } = await connectToMachineService();
|
|
167
167
|
try {
|
|
168
|
-
const { resolveSessionId } = await import('./commands-
|
|
168
|
+
const { resolveSessionId } = await import('./commands-CEazgqvj.mjs');
|
|
169
169
|
const sessions = await machine.listSessions();
|
|
170
170
|
const match = resolveSessionId(sessions, targetSessionId);
|
|
171
171
|
const fullTargetId = match.sessionId;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-
|
|
1
|
+
import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-DypcS01S.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -7,6 +7,7 @@ import 'url';
|
|
|
7
7
|
import 'child_process';
|
|
8
8
|
import 'crypto';
|
|
9
9
|
import 'node:fs';
|
|
10
|
+
import 'util';
|
|
10
11
|
import 'node:crypto';
|
|
11
12
|
import 'node:path';
|
|
12
13
|
import 'node:child_process';
|
|
@@ -43,7 +44,7 @@ async function main() {
|
|
|
43
44
|
console.error(`svamp daemon restart: ${err.message || err}`);
|
|
44
45
|
process.exit(1);
|
|
45
46
|
}
|
|
46
|
-
const { restartDaemon } = await import('./run-
|
|
47
|
+
const { restartDaemon } = await import('./run-DypcS01S.mjs').then(function (n) { return n.u; });
|
|
47
48
|
await restartDaemon();
|
|
48
49
|
process.exit(0);
|
|
49
50
|
}
|
|
@@ -279,7 +280,7 @@ async function main() {
|
|
|
279
280
|
console.error("svamp service: Service commands are not available in sandboxed sessions.");
|
|
280
281
|
process.exit(1);
|
|
281
282
|
}
|
|
282
|
-
const { handleServiceCommand } = await import('./commands-
|
|
283
|
+
const { handleServiceCommand } = await import('./commands-CY-HbTVN.mjs');
|
|
283
284
|
await handleServiceCommand();
|
|
284
285
|
} else if (subcommand === "serve") {
|
|
285
286
|
const { isSandboxed: isSandboxedServe } = await import('./sandboxDetect-DNTcbgWD.mjs');
|
|
@@ -287,7 +288,7 @@ async function main() {
|
|
|
287
288
|
console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
|
|
288
289
|
process.exit(1);
|
|
289
290
|
}
|
|
290
|
-
const { handleServeCommand } = await import('./serveCommands-
|
|
291
|
+
const { handleServeCommand } = await import('./serveCommands-CtAX-CMI.mjs');
|
|
291
292
|
await handleServeCommand();
|
|
292
293
|
process.exit(0);
|
|
293
294
|
} else if (subcommand === "process" || subcommand === "proc") {
|
|
@@ -296,7 +297,7 @@ async function main() {
|
|
|
296
297
|
console.error("svamp process: Process commands are not available in sandboxed sessions.");
|
|
297
298
|
process.exit(1);
|
|
298
299
|
}
|
|
299
|
-
const { processCommand } = await import('./commands-
|
|
300
|
+
const { processCommand } = await import('./commands-BFeHwsR-.mjs');
|
|
300
301
|
let machineId;
|
|
301
302
|
const processArgs = args.slice(1);
|
|
302
303
|
const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
|
|
@@ -314,7 +315,7 @@ async function main() {
|
|
|
314
315
|
} else if (!subcommand || subcommand === "start") {
|
|
315
316
|
await handleInteractiveCommand();
|
|
316
317
|
} else if (subcommand === "--version" || subcommand === "-v") {
|
|
317
|
-
const pkg = await import('./package-
|
|
318
|
+
const pkg = await import('./package-QQovcM_B.mjs').catch(() => ({ default: { version: "unknown" } }));
|
|
318
319
|
console.log(`svamp version: ${pkg.default.version}`);
|
|
319
320
|
} else {
|
|
320
321
|
console.error(`Unknown command: ${subcommand}`);
|
|
@@ -323,7 +324,7 @@ async function main() {
|
|
|
323
324
|
}
|
|
324
325
|
}
|
|
325
326
|
async function handleInteractiveCommand() {
|
|
326
|
-
const { runInteractive } = await import('./run-
|
|
327
|
+
const { runInteractive } = await import('./run-DB2WIjmZ.mjs');
|
|
327
328
|
const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
|
|
328
329
|
let directory = process.cwd();
|
|
329
330
|
let resumeSessionId;
|
|
@@ -368,7 +369,7 @@ async function handleAgentCommand() {
|
|
|
368
369
|
return;
|
|
369
370
|
}
|
|
370
371
|
if (agentArgs[0] === "list") {
|
|
371
|
-
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-
|
|
372
|
+
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-DypcS01S.mjs').then(function (n) { return n.p; });
|
|
372
373
|
console.log("Known agents:");
|
|
373
374
|
for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
|
|
374
375
|
console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
|
|
@@ -380,7 +381,7 @@ async function handleAgentCommand() {
|
|
|
380
381
|
console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
383
|
-
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-
|
|
384
|
+
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-DypcS01S.mjs').then(function (n) { return n.p; });
|
|
384
385
|
let cwd = process.cwd();
|
|
385
386
|
const filteredArgs = [];
|
|
386
387
|
for (let i = 0; i < agentArgs.length; i++) {
|
|
@@ -404,12 +405,12 @@ async function handleAgentCommand() {
|
|
|
404
405
|
console.log(`Starting ${config.agentName} agent in ${cwd}...`);
|
|
405
406
|
let backend;
|
|
406
407
|
if (KNOWN_MCP_AGENTS[config.agentName]) {
|
|
407
|
-
const { CodexMcpBackend } = await import('./run-
|
|
408
|
+
const { CodexMcpBackend } = await import('./run-DypcS01S.mjs').then(function (n) { return n.q; });
|
|
408
409
|
backend = new CodexMcpBackend({ cwd, log: logFn });
|
|
409
410
|
} else {
|
|
410
|
-
const { AcpBackend } = await import('./run-
|
|
411
|
-
const { GeminiTransport } = await import('./run-
|
|
412
|
-
const { DefaultTransport } = await import('./run-
|
|
411
|
+
const { AcpBackend } = await import('./run-DypcS01S.mjs').then(function (n) { return n.o; });
|
|
412
|
+
const { GeminiTransport } = await import('./run-DypcS01S.mjs').then(function (n) { return n.G; });
|
|
413
|
+
const { DefaultTransport } = await import('./run-DypcS01S.mjs').then(function (n) { return n.D; });
|
|
413
414
|
const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
|
|
414
415
|
backend = new AcpBackend({
|
|
415
416
|
agentName: config.agentName,
|
|
@@ -536,7 +537,7 @@ async function handleSessionCommand() {
|
|
|
536
537
|
process.exit(1);
|
|
537
538
|
}
|
|
538
539
|
}
|
|
539
|
-
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-
|
|
540
|
+
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-CEazgqvj.mjs');
|
|
540
541
|
const parseFlagStr = (flag, shortFlag) => {
|
|
541
542
|
for (let i = 1; i < sessionArgs.length; i++) {
|
|
542
543
|
if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
|
|
@@ -596,7 +597,7 @@ async function handleSessionCommand() {
|
|
|
596
597
|
allowDomain.push(sessionArgs[++i]);
|
|
597
598
|
}
|
|
598
599
|
}
|
|
599
|
-
const { parseShareArg } = await import('./commands-
|
|
600
|
+
const { parseShareArg } = await import('./commands-CEazgqvj.mjs');
|
|
600
601
|
const shareEntries = share.map((s) => parseShareArg(s));
|
|
601
602
|
await sessionSpawn(agent, dir, targetMachineId, {
|
|
602
603
|
message,
|
|
@@ -682,7 +683,7 @@ async function handleSessionCommand() {
|
|
|
682
683
|
console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
|
|
683
684
|
process.exit(1);
|
|
684
685
|
}
|
|
685
|
-
const { sessionApprove } = await import('./commands-
|
|
686
|
+
const { sessionApprove } = await import('./commands-CEazgqvj.mjs');
|
|
686
687
|
const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
687
688
|
await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
|
|
688
689
|
json: hasFlag("--json")
|
|
@@ -692,7 +693,7 @@ async function handleSessionCommand() {
|
|
|
692
693
|
console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
|
|
693
694
|
process.exit(1);
|
|
694
695
|
}
|
|
695
|
-
const { sessionDeny } = await import('./commands-
|
|
696
|
+
const { sessionDeny } = await import('./commands-CEazgqvj.mjs');
|
|
696
697
|
const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
697
698
|
await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
|
|
698
699
|
json: hasFlag("--json")
|
|
@@ -728,7 +729,7 @@ async function handleSessionCommand() {
|
|
|
728
729
|
console.error("Usage: svamp session set-title <title>");
|
|
729
730
|
process.exit(1);
|
|
730
731
|
}
|
|
731
|
-
const { sessionSetTitle } = await import('./agentCommands-
|
|
732
|
+
const { sessionSetTitle } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
732
733
|
await sessionSetTitle(title);
|
|
733
734
|
} else if (sessionSubcommand === "set-link") {
|
|
734
735
|
const url = sessionArgs[1];
|
|
@@ -737,7 +738,7 @@ async function handleSessionCommand() {
|
|
|
737
738
|
process.exit(1);
|
|
738
739
|
}
|
|
739
740
|
const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
740
|
-
const { sessionSetLink } = await import('./agentCommands-
|
|
741
|
+
const { sessionSetLink } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
741
742
|
await sessionSetLink(url, label);
|
|
742
743
|
} else if (sessionSubcommand === "notify") {
|
|
743
744
|
const message = sessionArgs[1];
|
|
@@ -746,7 +747,7 @@ async function handleSessionCommand() {
|
|
|
746
747
|
process.exit(1);
|
|
747
748
|
}
|
|
748
749
|
const level = parseFlagStr("--level") || "info";
|
|
749
|
-
const { sessionNotify } = await import('./agentCommands-
|
|
750
|
+
const { sessionNotify } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
750
751
|
await sessionNotify(message, level);
|
|
751
752
|
} else if (sessionSubcommand === "broadcast") {
|
|
752
753
|
const action = sessionArgs[1];
|
|
@@ -754,7 +755,7 @@ async function handleSessionCommand() {
|
|
|
754
755
|
console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
|
|
755
756
|
process.exit(1);
|
|
756
757
|
}
|
|
757
|
-
const { sessionBroadcast } = await import('./agentCommands-
|
|
758
|
+
const { sessionBroadcast } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
758
759
|
await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
|
|
759
760
|
} else if (sessionSubcommand === "inbox") {
|
|
760
761
|
const inboxSubcmd = sessionArgs[1];
|
|
@@ -765,7 +766,7 @@ async function handleSessionCommand() {
|
|
|
765
766
|
process.exit(1);
|
|
766
767
|
}
|
|
767
768
|
if (agentSessionId) {
|
|
768
|
-
const { inboxSend } = await import('./agentCommands-
|
|
769
|
+
const { inboxSend } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
769
770
|
await inboxSend(sessionArgs[2], {
|
|
770
771
|
body: sessionArgs[3],
|
|
771
772
|
subject: parseFlagStr("--subject"),
|
|
@@ -780,7 +781,7 @@ async function handleSessionCommand() {
|
|
|
780
781
|
}
|
|
781
782
|
} else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
|
|
782
783
|
if (agentSessionId && !sessionArgs[2]) {
|
|
783
|
-
const { inboxList } = await import('./agentCommands-
|
|
784
|
+
const { inboxList } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
784
785
|
await inboxList({
|
|
785
786
|
unread: hasFlag("--unread"),
|
|
786
787
|
limit: parseFlagInt("--limit"),
|
|
@@ -802,7 +803,7 @@ async function handleSessionCommand() {
|
|
|
802
803
|
process.exit(1);
|
|
803
804
|
}
|
|
804
805
|
if (agentSessionId && !sessionArgs[3]) {
|
|
805
|
-
const { inboxList } = await import('./agentCommands-
|
|
806
|
+
const { inboxList } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
806
807
|
await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
|
|
807
808
|
} else if (sessionArgs[3]) {
|
|
808
809
|
await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
|
|
@@ -812,7 +813,7 @@ async function handleSessionCommand() {
|
|
|
812
813
|
}
|
|
813
814
|
} else if (inboxSubcmd === "reply") {
|
|
814
815
|
if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
|
|
815
|
-
const { inboxReply } = await import('./agentCommands-
|
|
816
|
+
const { inboxReply } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
816
817
|
await inboxReply(sessionArgs[2], sessionArgs[3]);
|
|
817
818
|
} else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
|
|
818
819
|
await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
|
|
@@ -848,7 +849,7 @@ async function handleMachineCommand() {
|
|
|
848
849
|
return;
|
|
849
850
|
}
|
|
850
851
|
if (machineSubcommand === "share") {
|
|
851
|
-
const { machineShare } = await import('./commands-
|
|
852
|
+
const { machineShare } = await import('./commands-CEazgqvj.mjs');
|
|
852
853
|
let machineId;
|
|
853
854
|
const shareArgs = [];
|
|
854
855
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
@@ -878,7 +879,7 @@ async function handleMachineCommand() {
|
|
|
878
879
|
}
|
|
879
880
|
await machineShare(machineId, { add, remove, list, configPath, showConfig });
|
|
880
881
|
} else if (machineSubcommand === "exec") {
|
|
881
|
-
const { machineExec } = await import('./commands-
|
|
882
|
+
const { machineExec } = await import('./commands-CEazgqvj.mjs');
|
|
882
883
|
let machineId;
|
|
883
884
|
let cwd;
|
|
884
885
|
const cmdParts = [];
|
|
@@ -898,7 +899,7 @@ async function handleMachineCommand() {
|
|
|
898
899
|
}
|
|
899
900
|
await machineExec(machineId, command, cwd);
|
|
900
901
|
} else if (machineSubcommand === "info") {
|
|
901
|
-
const { machineInfo } = await import('./commands-
|
|
902
|
+
const { machineInfo } = await import('./commands-CEazgqvj.mjs');
|
|
902
903
|
let machineId;
|
|
903
904
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
904
905
|
if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
|
|
@@ -918,10 +919,10 @@ async function handleMachineCommand() {
|
|
|
918
919
|
level = machineArgs[++i];
|
|
919
920
|
}
|
|
920
921
|
}
|
|
921
|
-
const { machineNotify } = await import('./agentCommands-
|
|
922
|
+
const { machineNotify } = await import('./agentCommands-BH6CIqc7.mjs');
|
|
922
923
|
await machineNotify(message, level);
|
|
923
924
|
} else if (machineSubcommand === "ls") {
|
|
924
|
-
const { machineLs } = await import('./commands-
|
|
925
|
+
const { machineLs } = await import('./commands-CEazgqvj.mjs');
|
|
925
926
|
let machineId;
|
|
926
927
|
let showHidden = false;
|
|
927
928
|
let path;
|
|
@@ -1388,7 +1389,7 @@ async function applyClaudeAuthFlags(argv) {
|
|
|
1388
1389
|
"--use-hypha-proxy, --use-claude-login, and --anthropic-base-url/--anthropic-api-key are mutually exclusive"
|
|
1389
1390
|
);
|
|
1390
1391
|
}
|
|
1391
|
-
const mod = await import('./run-
|
|
1392
|
+
const mod = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
|
|
1392
1393
|
if (hasHypha) {
|
|
1393
1394
|
mod.setClaudeAuthHyphaProxy();
|
|
1394
1395
|
console.log("Claude auth configured: hypha-proxy (uses HYPHA_TOKEN live at each spawn).");
|
|
@@ -1426,7 +1427,7 @@ async function applyDaemonShareFlag(argv) {
|
|
|
1426
1427
|
}
|
|
1427
1428
|
}
|
|
1428
1429
|
if (collected.length === 0) return;
|
|
1429
|
-
const { updateEnvFile } = await import('./run-
|
|
1430
|
+
const { updateEnvFile } = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
|
|
1430
1431
|
const seen = /* @__PURE__ */ new Set();
|
|
1431
1432
|
const deduped = collected.filter((e) => {
|
|
1432
1433
|
const k = e.toLowerCase();
|
|
@@ -1439,7 +1440,7 @@ async function applyDaemonShareFlag(argv) {
|
|
|
1439
1440
|
}
|
|
1440
1441
|
async function handleDaemonAuthCommand(argv) {
|
|
1441
1442
|
const sub = (argv[0] || "status").toLowerCase();
|
|
1442
|
-
const mod = await import('./run-
|
|
1443
|
+
const mod = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
|
|
1443
1444
|
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
1444
1445
|
console.log(`
|
|
1445
1446
|
svamp daemon auth \u2014 Configure how Claude subprocesses authenticate
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { connectAndGetMachine } from './commands-
|
|
3
|
+
import { connectAndGetMachine } from './commands-CEazgqvj.mjs';
|
|
4
4
|
import 'node:fs';
|
|
5
5
|
import 'node:child_process';
|
|
6
6
|
import 'node:path';
|
|
7
7
|
import 'node:os';
|
|
8
|
-
import './run-
|
|
8
|
+
import './run-DypcS01S.mjs';
|
|
9
9
|
import 'os';
|
|
10
10
|
import 'fs/promises';
|
|
11
11
|
import 'url';
|
|
12
12
|
import 'child_process';
|
|
13
13
|
import 'crypto';
|
|
14
|
+
import 'util';
|
|
14
15
|
import 'node:crypto';
|
|
15
16
|
import '@agentclientprotocol/sdk';
|
|
16
17
|
import '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -149,6 +150,7 @@ function normalizeSpec(raw) {
|
|
|
149
150
|
console.error("Error: spec.command is required");
|
|
150
151
|
process.exit(1);
|
|
151
152
|
}
|
|
153
|
+
const sessionId = typeof raw.sessionId === "string" ? raw.sessionId : process.env.SVAMP_SESSION_ID || void 0;
|
|
152
154
|
return {
|
|
153
155
|
name: raw.name,
|
|
154
156
|
command: raw.command,
|
|
@@ -169,7 +171,8 @@ function normalizeSpec(raw) {
|
|
|
169
171
|
// ttl: 0 / negative / missing all mean "no TTL, run forever". Any positive value is seconds.
|
|
170
172
|
ttl: raw.ttl !== void 0 && Number(raw.ttl) > 0 ? Number(raw.ttl) : void 0,
|
|
171
173
|
serviceGroup: raw.serviceGroup,
|
|
172
|
-
ports: Array.isArray(raw.ports) ? raw.ports.map(Number) : void 0
|
|
174
|
+
ports: Array.isArray(raw.ports) ? raw.ports.map(Number) : void 0,
|
|
175
|
+
...sessionId ? { sessionId } : {}
|
|
173
176
|
};
|
|
174
177
|
}
|
|
175
178
|
function statusIcon(status) {
|
|
@@ -439,6 +442,7 @@ async function startCommand(args, machineId) {
|
|
|
439
442
|
}
|
|
440
443
|
env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
441
444
|
}
|
|
445
|
+
const sessionId = process.env.SVAMP_SESSION_ID || void 0;
|
|
442
446
|
const spec = {
|
|
443
447
|
name: idOrName,
|
|
444
448
|
command,
|
|
@@ -449,7 +453,8 @@ async function startCommand(args, machineId) {
|
|
|
449
453
|
maxRestarts,
|
|
450
454
|
restartDelay,
|
|
451
455
|
...probe ? { probe } : {},
|
|
452
|
-
...ttl !== void 0 ? { ttl } : {}
|
|
456
|
+
...ttl !== void 0 ? { ttl } : {},
|
|
457
|
+
...sessionId ? { sessionId } : {}
|
|
453
458
|
};
|
|
454
459
|
const saveFile = getFlag(flagArgs, "--save");
|
|
455
460
|
const { server, machine } = await connectAndGetMachine(machineId);
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { n as normalizeAllowedUser, l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha, i as buildSessionShareUrl, j as buildMachineShareUrl } from './run-
|
|
5
|
+
import { n as normalizeAllowedUser, l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha, i as buildSessionShareUrl, j as buildMachineShareUrl } from './run-DypcS01S.mjs';
|
|
6
6
|
import 'os';
|
|
7
7
|
import 'fs/promises';
|
|
8
8
|
import 'fs';
|
|
@@ -10,6 +10,7 @@ import 'path';
|
|
|
10
10
|
import 'url';
|
|
11
11
|
import 'child_process';
|
|
12
12
|
import 'crypto';
|
|
13
|
+
import 'util';
|
|
13
14
|
import 'node:crypto';
|
|
14
15
|
import '@agentclientprotocol/sdk';
|
|
15
16
|
import '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -68,7 +68,7 @@ async function serviceExpose(args) {
|
|
|
68
68
|
});
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
71
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
72
72
|
const { server, machine } = await connectAndGetMachine();
|
|
73
73
|
try {
|
|
74
74
|
const status = await machine.tunnelStart({
|
|
@@ -132,7 +132,7 @@ async function serviceServe(args) {
|
|
|
132
132
|
}
|
|
133
133
|
async function serviceList(_args) {
|
|
134
134
|
try {
|
|
135
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
135
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
136
136
|
const { server, machine } = await connectAndGetMachine();
|
|
137
137
|
try {
|
|
138
138
|
const tunnels = await machine.tunnelList({});
|
|
@@ -161,7 +161,7 @@ async function serviceDelete(args) {
|
|
|
161
161
|
process.exit(1);
|
|
162
162
|
}
|
|
163
163
|
try {
|
|
164
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
164
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
165
165
|
const { server, machine } = await connectAndGetMachine();
|
|
166
166
|
try {
|
|
167
167
|
await machine.tunnelStop({ name });
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-
|
|
1
|
+
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-DypcS01S.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -7,6 +7,7 @@ import 'url';
|
|
|
7
7
|
import 'child_process';
|
|
8
8
|
import 'crypto';
|
|
9
9
|
import 'node:fs';
|
|
10
|
+
import 'util';
|
|
10
11
|
import 'node:crypto';
|
|
11
12
|
import 'node:path';
|
|
12
13
|
import 'node:child_process';
|
|
@@ -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-DypcS01S.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';
|
|
@@ -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();
|
|
@@ -6758,6 +7007,22 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
6758
7007
|
logger.log(`[svampConfig] Title updated: "${newTitle}"`);
|
|
6759
7008
|
}
|
|
6760
7009
|
}
|
|
7010
|
+
if (config.custom_title === null || config.custom_title === "") {
|
|
7011
|
+
if (meta.customTitle) {
|
|
7012
|
+
setMetadata((m) => {
|
|
7013
|
+
const next = { ...m };
|
|
7014
|
+
delete next.customTitle;
|
|
7015
|
+
return next;
|
|
7016
|
+
});
|
|
7017
|
+
logger.log(`[svampConfig] customTitle cleared`);
|
|
7018
|
+
}
|
|
7019
|
+
} else if (typeof config.custom_title === "string" && config.custom_title.trim()) {
|
|
7020
|
+
const newCustom = config.custom_title.trim();
|
|
7021
|
+
if (meta.customTitle !== newCustom) {
|
|
7022
|
+
setMetadata((m) => ({ ...m, customTitle: newCustom }));
|
|
7023
|
+
logger.log(`[svampConfig] customTitle updated: "${newCustom}"`);
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
6761
7026
|
if (config.session_link && typeof config.session_link.url === "string" && config.session_link.url.trim()) {
|
|
6762
7027
|
const url = config.session_link.url.trim();
|
|
6763
7028
|
const label = config.session_link.label || "View";
|
|
@@ -7425,7 +7690,7 @@ async function startDaemon(options) {
|
|
|
7425
7690
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
7426
7691
|
saveExposedTunnels(list);
|
|
7427
7692
|
}
|
|
7428
|
-
const { ServeManager } = await import('./serveManager-
|
|
7693
|
+
const { ServeManager } = await import('./serveManager-G9PiLNKm.mjs');
|
|
7429
7694
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
7430
7695
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
7431
7696
|
});
|
|
@@ -7722,6 +7987,9 @@ async function startDaemon(options) {
|
|
|
7722
7987
|
let isKillingClaude = false;
|
|
7723
7988
|
let isRestartingClaude = false;
|
|
7724
7989
|
let isSwitchingMode = false;
|
|
7990
|
+
const OVERLOAD_BAIL_THRESHOLD = 3;
|
|
7991
|
+
let consecutiveOverloadRetries = 0;
|
|
7992
|
+
let overloadBailedThisTurn = false;
|
|
7725
7993
|
let checkSvampConfig;
|
|
7726
7994
|
let cleanupSvampConfig;
|
|
7727
7995
|
const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
@@ -7977,13 +8245,21 @@ async function startDaemon(options) {
|
|
|
7977
8245
|
logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
|
|
7978
8246
|
startupFailureRetryPending = true;
|
|
7979
8247
|
lastErrorMessagePushed = true;
|
|
8248
|
+
} else if (overloadBailedThisTurn) {
|
|
8249
|
+
logger.log(`[Session ${sessionId}] Suppressing duplicate error \u2014 overload hint already pushed for this turn`);
|
|
8250
|
+
lastErrorMessagePushed = true;
|
|
7980
8251
|
} else {
|
|
7981
8252
|
const lower = resultText.toLowerCase();
|
|
8253
|
+
const isOverload = msg.api_error_status === 529 || lower.includes("529") || lower.includes("overload");
|
|
7982
8254
|
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
7983
8255
|
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
7984
8256
|
const isBillingIssue = lower.includes("credit") || lower.includes("balance") || lower.includes("billing") || lower.includes("quota") || lower.includes("subscription") || lower.includes("payment");
|
|
7985
8257
|
let hint = "";
|
|
7986
|
-
if (
|
|
8258
|
+
if (isOverload) {
|
|
8259
|
+
const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
|
|
8260
|
+
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`";
|
|
8261
|
+
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;
|
|
8262
|
+
} else if (isBillingIssue) {
|
|
7987
8263
|
hint = "\n\nCheck your Claude account credits or subscription at https://console.anthropic.com.";
|
|
7988
8264
|
} else if (isLoginIssue) {
|
|
7989
8265
|
checkAndRefreshOAuthToken(true, logger).then((r) => {
|
|
@@ -8271,6 +8547,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8271
8547
|
sessionService.pushMessage(msg, "agent");
|
|
8272
8548
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
8273
8549
|
lastAssistantText = "";
|
|
8550
|
+
consecutiveOverloadRetries = 0;
|
|
8551
|
+
overloadBailedThisTurn = false;
|
|
8274
8552
|
if (!userMessagePending) {
|
|
8275
8553
|
turnInitiatedByUser = false;
|
|
8276
8554
|
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
@@ -8315,6 +8593,34 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8315
8593
|
sessionService.updateMetadata(sessionMetadata);
|
|
8316
8594
|
}
|
|
8317
8595
|
sessionService.pushMessage(msg, "session");
|
|
8596
|
+
} else if (msg.type === "system" && msg.subtype === "api_retry") {
|
|
8597
|
+
if (msg.error_status === 529) {
|
|
8598
|
+
consecutiveOverloadRetries++;
|
|
8599
|
+
if (consecutiveOverloadRetries >= OVERLOAD_BAIL_THRESHOLD && !overloadBailedThisTurn) {
|
|
8600
|
+
overloadBailedThisTurn = true;
|
|
8601
|
+
const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
|
|
8602
|
+
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`";
|
|
8603
|
+
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.
|
|
8604
|
+
|
|
8605
|
+
**Try:**
|
|
8606
|
+
\u2022 Start a fresh session (different conversation id \u2192 different shard).
|
|
8607
|
+
\u2022 Wait a minute and resend.
|
|
8608
|
+
\u2022 Check https://status.claude.com.` + proxyHint;
|
|
8609
|
+
logger.log(`[Session ${sessionId}] Bailing on ${consecutiveOverloadRetries}\xD7 consecutive 529 retries \u2014 sending interrupt`);
|
|
8610
|
+
sessionService.pushMessage({ type: "message", message: hint, level: "warning" }, "event");
|
|
8611
|
+
try {
|
|
8612
|
+
if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
|
|
8613
|
+
const interruptMsg = JSON.stringify({ type: "control_request", request: { type: "interrupt" } });
|
|
8614
|
+
claudeProcess.stdin.write(interruptMsg + "\n");
|
|
8615
|
+
}
|
|
8616
|
+
} catch (err) {
|
|
8617
|
+
logger.log(`[Session ${sessionId}] Failed to send overload interrupt: ${err.message}`);
|
|
8618
|
+
}
|
|
8619
|
+
}
|
|
8620
|
+
} else {
|
|
8621
|
+
consecutiveOverloadRetries = 0;
|
|
8622
|
+
}
|
|
8623
|
+
sessionService.pushMessage(msg, "agent");
|
|
8318
8624
|
} else if (msg.type === "system" && msg.subtype === "task_notification" && msg.status === "completed") {
|
|
8319
8625
|
backgroundTaskCount = Math.max(0, backgroundTaskCount - 1);
|
|
8320
8626
|
if (backgroundTaskNames.length > 0) {
|
|
@@ -8769,6 +9075,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8769
9075
|
onMetadataUpdate: (newMeta) => {
|
|
8770
9076
|
const oldTitle = sessionMetadata.summary?.text;
|
|
8771
9077
|
const newTitle = newMeta.summary?.text;
|
|
9078
|
+
const oldCustomTitle = sessionMetadata.customTitle;
|
|
9079
|
+
const newCustomTitle = newMeta.customTitle;
|
|
8772
9080
|
const oldLink = sessionMetadata.sessionLink;
|
|
8773
9081
|
const newLink = newMeta.sessionLink;
|
|
8774
9082
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
@@ -8788,6 +9096,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
8788
9096
|
};
|
|
8789
9097
|
const cfgPatch = {};
|
|
8790
9098
|
if (newTitle !== oldTitle) cfgPatch.title = newTitle ?? null;
|
|
9099
|
+
if (newCustomTitle !== oldCustomTitle) cfgPatch.custom_title = newCustomTitle ?? null;
|
|
8791
9100
|
if (newLink?.url !== oldLink?.url || newLink?.label !== oldLink?.label) {
|
|
8792
9101
|
cfgPatch.session_link = newLink?.url ? { url: newLink.url, label: newLink.label || "View" } : null;
|
|
8793
9102
|
}
|
|
@@ -9253,6 +9562,8 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9253
9562
|
onMetadataUpdate: (newMeta) => {
|
|
9254
9563
|
const oldTitleAcp = sessionMetadata.summary?.text;
|
|
9255
9564
|
const newTitleAcp = newMeta.summary?.text;
|
|
9565
|
+
const oldCustomTitleAcp = sessionMetadata.customTitle;
|
|
9566
|
+
const newCustomTitleAcp = newMeta.customTitle;
|
|
9256
9567
|
const oldLinkAcp = sessionMetadata.sessionLink;
|
|
9257
9568
|
const newLinkAcp = newMeta.sessionLink;
|
|
9258
9569
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
@@ -9270,6 +9581,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9270
9581
|
};
|
|
9271
9582
|
const cfgPatchAcp = {};
|
|
9272
9583
|
if (newTitleAcp !== oldTitleAcp) cfgPatchAcp.title = newTitleAcp ?? null;
|
|
9584
|
+
if (newCustomTitleAcp !== oldCustomTitleAcp) cfgPatchAcp.custom_title = newCustomTitleAcp ?? null;
|
|
9273
9585
|
if (newLinkAcp?.url !== oldLinkAcp?.url || newLinkAcp?.label !== oldLinkAcp?.label) {
|
|
9274
9586
|
cfgPatchAcp.session_link = newLinkAcp?.url ? { url: newLinkAcp.url, label: newLinkAcp.label || "View" } : null;
|
|
9275
9587
|
}
|
|
@@ -9798,6 +10110,14 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
9798
10110
|
}
|
|
9799
10111
|
return ids;
|
|
9800
10112
|
},
|
|
10113
|
+
getSessionPid: (sessionId) => {
|
|
10114
|
+
for (const [, session] of pidToTrackedSession) {
|
|
10115
|
+
if (session.svampSessionId === sessionId && !session.stopped) {
|
|
10116
|
+
return session.childProcess?.pid;
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
10119
|
+
return void 0;
|
|
10120
|
+
},
|
|
9801
10121
|
supervisor,
|
|
9802
10122
|
tunnels,
|
|
9803
10123
|
serveManager,
|
|
@@ -54,17 +54,20 @@ async function handleServeCommand() {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
async function serveAdd(args, machineId) {
|
|
57
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
57
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
58
58
|
const pos = positionalArgs(args);
|
|
59
59
|
const name = pos[0];
|
|
60
60
|
if (!name) {
|
|
61
|
-
console.error("Usage: svamp serve [add] <name> [directory] [--public | --access email1,email2]");
|
|
61
|
+
console.error("Usage: svamp serve [add] <name> [directory] [--public | --owner | --access email1,email2]");
|
|
62
|
+
console.error(" Default: capability URL (link mode) \u2014 anyone with the URL can view, no login required.");
|
|
62
63
|
process.exit(1);
|
|
63
64
|
}
|
|
64
65
|
const directory = path.resolve(pos[1] || ".");
|
|
65
|
-
let access = "
|
|
66
|
+
let access = "link";
|
|
66
67
|
if (hasFlag(args, "--public")) {
|
|
67
68
|
access = "public";
|
|
69
|
+
} else if (hasFlag(args, "--owner")) {
|
|
70
|
+
access = "owner";
|
|
68
71
|
} else {
|
|
69
72
|
const accessFlag = getFlag(args, "--access");
|
|
70
73
|
if (accessFlag) {
|
|
@@ -75,8 +78,12 @@ async function serveAdd(args, machineId) {
|
|
|
75
78
|
try {
|
|
76
79
|
const result = await machine.serveAdd({ name, directory, access });
|
|
77
80
|
console.log(`Mount added: ${name} \u2192 ${directory}`);
|
|
78
|
-
|
|
81
|
+
const accessLabel = access === "public" ? "public (no auth)" : access === "link" ? "link (capability URL \u2014 anyone with the URL can view)" : access === "owner" ? "owner only (Hypha login)" : `${access.join(", ")} (Hypha login)`;
|
|
82
|
+
console.log(`Access: ${accessLabel}`);
|
|
79
83
|
console.log(`URL: ${result.url}`);
|
|
84
|
+
if (access === "link") {
|
|
85
|
+
console.log(`(URL embeds a long capability token; keep it secret to keep the mount private.)`);
|
|
86
|
+
}
|
|
80
87
|
} catch (err) {
|
|
81
88
|
console.error(`Error: ${err.message || err}`);
|
|
82
89
|
process.exit(1);
|
|
@@ -86,7 +93,7 @@ async function serveAdd(args, machineId) {
|
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
async function serveApply(args, machineId) {
|
|
89
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
96
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
90
97
|
const fs = await import('fs');
|
|
91
98
|
const yaml = await import('yaml');
|
|
92
99
|
const file = positionalArgs(args)[0];
|
|
@@ -107,7 +114,10 @@ async function serveApply(args, machineId) {
|
|
|
107
114
|
console.error(" warmup_timeout_ms: 30000");
|
|
108
115
|
console.error(" idle_timeout_sec: 600 # 0 = always running");
|
|
109
116
|
console.error(" wake_on_request: true # spawn lazily on first request");
|
|
110
|
-
console.error(" access:
|
|
117
|
+
console.error(" access: link # link (default) | public | owner | [emails]");
|
|
118
|
+
console.error(" # link \u2014 capability URL, anyone with the URL can view");
|
|
119
|
+
console.error(" # public \u2014 fully open, short URL");
|
|
120
|
+
console.error(" # owner / [emails] \u2014 Hypha login required");
|
|
111
121
|
process.exit(1);
|
|
112
122
|
}
|
|
113
123
|
if (!fs.existsSync(file)) {
|
|
@@ -150,7 +160,8 @@ async function serveApply(args, machineId) {
|
|
|
150
160
|
directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
|
|
151
161
|
process: proc,
|
|
152
162
|
sessionId: parsed.sessionId ?? parsed.session_id,
|
|
153
|
-
access: parsed.access ?? "
|
|
163
|
+
access: parsed.access ?? "link",
|
|
164
|
+
// default flipped from 'owner' to 'link' (capability URL)
|
|
154
165
|
ownerEmail: parsed.ownerEmail ?? parsed.owner_email
|
|
155
166
|
};
|
|
156
167
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
@@ -171,7 +182,7 @@ async function serveApply(args, machineId) {
|
|
|
171
182
|
}
|
|
172
183
|
}
|
|
173
184
|
async function serveRemove(args, machineId) {
|
|
174
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
185
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
175
186
|
const pos = positionalArgs(args);
|
|
176
187
|
const name = pos[0];
|
|
177
188
|
if (!name) {
|
|
@@ -191,7 +202,7 @@ async function serveRemove(args, machineId) {
|
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
async function serveList(args, machineId) {
|
|
194
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
205
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
195
206
|
const all = hasFlag(args, "--all", "-a");
|
|
196
207
|
const json = hasFlag(args, "--json");
|
|
197
208
|
const sessionId = getFlag(args, "--session");
|
|
@@ -224,7 +235,7 @@ async function serveList(args, machineId) {
|
|
|
224
235
|
}
|
|
225
236
|
}
|
|
226
237
|
async function serveInfo(machineId) {
|
|
227
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
238
|
+
const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
|
|
228
239
|
const { machine, server } = await connectAndGetMachine(machineId);
|
|
229
240
|
try {
|
|
230
241
|
const info = await machine.serveInfo();
|
|
@@ -258,10 +269,14 @@ Usage:
|
|
|
258
269
|
svamp serve list [--all] [--json] List mounts (default: current session only)
|
|
259
270
|
svamp serve info Show server status and URL
|
|
260
271
|
|
|
261
|
-
Access
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
Access tiers (default: link):
|
|
273
|
+
(no flag) Capability URL \u2014 anyone with the URL can view.
|
|
274
|
+
URL embeds a long random token (~192-bit entropy)
|
|
275
|
+
as the first path segment. No login required.
|
|
276
|
+
Best for embedding in agent artifact iframes.
|
|
277
|
+
--public Fully public \u2014 no auth, short URL.
|
|
278
|
+
--owner Hypha login required, owner-only.
|
|
279
|
+
--access email1,email2 Hypha login required, specific allowed emails.
|
|
265
280
|
|
|
266
281
|
Options:
|
|
267
282
|
-m, --machine <id> Target a specific machine
|
|
@@ -270,12 +285,13 @@ Options:
|
|
|
270
285
|
--json Output as JSON
|
|
271
286
|
|
|
272
287
|
Examples:
|
|
273
|
-
svamp serve my-report ./output
|
|
274
|
-
svamp serve dashboard ./dist --public
|
|
275
|
-
svamp serve data ./csv --
|
|
276
|
-
svamp serve
|
|
277
|
-
svamp serve
|
|
278
|
-
svamp serve
|
|
288
|
+
svamp serve my-report ./output # Default: capability URL
|
|
289
|
+
svamp serve dashboard ./dist --public # Anyone, short URL
|
|
290
|
+
svamp serve data ./csv --owner # Hypha login required
|
|
291
|
+
svamp serve data ./csv --access a@x.com,b@y.com # Specific Hypha users
|
|
292
|
+
svamp serve apply my-app.yaml # Declarative apply
|
|
293
|
+
svamp serve list --all # Show all mounts
|
|
294
|
+
svamp serve remove my-report # Stop serving
|
|
279
295
|
|
|
280
296
|
Declarative apply (svamp serve apply <yaml>):
|
|
281
297
|
name: my-app
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as http from 'http';
|
|
4
5
|
import * as net from 'net';
|
|
5
6
|
import * as path from 'path';
|
|
6
|
-
import { S as ServeAuth, h as hasCookieToken } from './run-
|
|
7
|
+
import { S as ServeAuth, h as hasCookieToken } from './run-DypcS01S.mjs';
|
|
7
8
|
import 'os';
|
|
8
9
|
import 'fs/promises';
|
|
9
10
|
import 'url';
|
|
10
|
-
import 'crypto';
|
|
11
11
|
import 'node:fs';
|
|
12
|
+
import 'util';
|
|
12
13
|
import 'node:crypto';
|
|
13
14
|
import 'node:path';
|
|
14
15
|
import 'node:child_process';
|
|
@@ -21,6 +22,9 @@ import 'zod';
|
|
|
21
22
|
import 'node:fs/promises';
|
|
22
23
|
import 'node:util';
|
|
23
24
|
|
|
25
|
+
function generateLinkToken() {
|
|
26
|
+
return crypto.randomBytes(16).toString("hex");
|
|
27
|
+
}
|
|
24
28
|
function findFreePort() {
|
|
25
29
|
return new Promise((resolve, reject) => {
|
|
26
30
|
const srv = net.createServer();
|
|
@@ -88,7 +92,7 @@ class ServeManager {
|
|
|
88
92
|
* Throws if a mount with the same name already exists, preserving the
|
|
89
93
|
* pre-existing semantics that callers may depend on.
|
|
90
94
|
*/
|
|
91
|
-
async addMount(name, directory, sessionId, access = "
|
|
95
|
+
async addMount(name, directory, sessionId, access = "link", ownerEmail) {
|
|
92
96
|
if (this.mounts.has(name)) {
|
|
93
97
|
throw new Error(`Mount '${name}' already exists. Remove it first or choose a different name.`);
|
|
94
98
|
}
|
|
@@ -123,13 +127,18 @@ class ServeManager {
|
|
|
123
127
|
if (this.mounts.has(spec.name)) {
|
|
124
128
|
await this.removeMount(spec.name);
|
|
125
129
|
}
|
|
130
|
+
const access = spec.access ?? "link";
|
|
126
131
|
const mount = {
|
|
127
132
|
name: spec.name,
|
|
128
133
|
directory: resolvedDir,
|
|
129
134
|
process: spec.process,
|
|
130
135
|
sessionId: spec.sessionId,
|
|
131
136
|
ownerEmail: spec.ownerEmail,
|
|
132
|
-
access
|
|
137
|
+
access,
|
|
138
|
+
// Generate a capability token if access is 'link' and we don't
|
|
139
|
+
// already have one from persisted state (token is stable across
|
|
140
|
+
// restarts).
|
|
141
|
+
linkToken: access === "link" ? spec.linkToken || generateLinkToken() : void 0,
|
|
133
142
|
addedAt: Date.now()
|
|
134
143
|
};
|
|
135
144
|
this.mounts.set(spec.name, mount);
|
|
@@ -432,11 +441,18 @@ class ServeManager {
|
|
|
432
441
|
}, 3e4);
|
|
433
442
|
}
|
|
434
443
|
// ── Internal ─────────────────────────────────────────────────────────
|
|
435
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* Get the public URL for a mount (mount-specific subdomain).
|
|
446
|
+
* For `access: 'link'` mounts, the subdomain itself carries the capability —
|
|
447
|
+
* the tunnel registers a long random suffix (~128 bits of entropy), so the
|
|
448
|
+
* URL is identical in shape to other tiers, just longer. Content serves
|
|
449
|
+
* from the root, so HTML with absolute paths (`/style.css`, `/app.js`)
|
|
450
|
+
* works unchanged.
|
|
451
|
+
*/
|
|
436
452
|
getMountUrl(name) {
|
|
437
453
|
const tunnel = this.mountTunnels.get(name);
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
454
|
+
const tunnelUrl = tunnel?.getUrls().get(this.port);
|
|
455
|
+
if (tunnelUrl) return `${tunnelUrl}/`;
|
|
440
456
|
if (this.port) return `http://127.0.0.1:${this.port}/${name}/`;
|
|
441
457
|
return null;
|
|
442
458
|
}
|
|
@@ -523,7 +539,7 @@ class ServeManager {
|
|
|
523
539
|
res.end(html);
|
|
524
540
|
return;
|
|
525
541
|
}
|
|
526
|
-
if (mount && mount.access !== "public") {
|
|
542
|
+
if (mount && mount.access !== "public" && mount.access !== "link") {
|
|
527
543
|
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
528
544
|
const allowed = this.auth ? this.auth.isAuthorized(userEmail, mount.access, mount.ownerEmail) : false;
|
|
529
545
|
if (!allowed) {
|
|
@@ -668,12 +684,15 @@ class ServeManager {
|
|
|
668
684
|
if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
|
|
669
685
|
const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
670
686
|
const tunnelName = `static-${subdomainSafe}`;
|
|
687
|
+
const mount = this.mounts.get(mountName);
|
|
688
|
+
const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
|
|
671
689
|
try {
|
|
672
690
|
const { FrpcTunnel } = await import('./frpc-j60b46eU.mjs');
|
|
673
691
|
let tunnel;
|
|
674
692
|
tunnel = new FrpcTunnel({
|
|
675
693
|
name: tunnelName,
|
|
676
694
|
ports: [this.port],
|
|
695
|
+
subdomains: subdomainOverride,
|
|
677
696
|
// End-to-end probe: the daemon's health loop watches probe.ok
|
|
678
697
|
// to detect ghosted tunnel registrations (frpc says "connected"
|
|
679
698
|
// but no traffic actually flows). The sentinel route is served
|