svamp-cli 0.2.342 → 0.2.356

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bin/skills/artifact/SKILL.md +19 -5
  2. package/bin/skills/crew/SKILL.md +62 -13
  3. package/dist/DefaultTransport-vfp78fug.mjs +47 -0
  4. package/dist/GeminiTransport-3OkhtFRO.mjs +130 -0
  5. package/dist/KimiTransport-D8n009Tr.mjs +141 -0
  6. package/dist/acpAgentConfig-CObOPlA8.mjs +48 -0
  7. package/dist/acpBackend-UaBD4zL3.mjs +925 -0
  8. package/dist/{adminCommands-CRTBDZan.mjs → adminCommands-BvsYnRDA.mjs} +2 -18
  9. package/dist/{agentCommands-BuO988tJ.mjs → agentCommands-D91RcjqQ.mjs} +6 -23
  10. package/dist/api-BHjyC0Ph.mjs +157 -0
  11. package/dist/{auth--WS0wezs.mjs → auth-CRz8TsEl.mjs} +1 -22
  12. package/dist/bootStatus-DqtNgJch.mjs +143 -0
  13. package/dist/claudeAuth-C0wc1GTM.mjs +291 -0
  14. package/dist/cli.mjs +2948 -5
  15. package/dist/codexAppServerBackend-VGsYtbqg.mjs +786 -0
  16. package/dist/codexProvider-BtxYc5Rj.mjs +68 -0
  17. package/dist/{commands-DDuGBSsz.mjs → commands-B-yczgz_.mjs} +115 -49
  18. package/dist/{commands-DU4B-4Gh.mjs → commands-BMeDrpSv.mjs} +55 -25
  19. package/dist/{commands-DXLOKJOj.mjs → commands-BbmypSgx.mjs} +62 -27
  20. package/dist/{commands-C0HQiu-S.mjs → commands-BkdbelFo.mjs} +78 -38
  21. package/dist/{commands-DXM17M6n.mjs → commands-C13-yE2R.mjs} +42 -19
  22. package/dist/{commands-BKb1g8Gq.mjs → commands-CoOc4qiw.mjs} +20 -30
  23. package/dist/{commands-BsKgZ5sQ.mjs → commands-DW5RnHZw.mjs} +2 -19
  24. package/dist/{commands-Cwxy-C5N.mjs → commands-sPvRcG3J.mjs} +10 -32
  25. package/dist/cron-hHGdb5kU.mjs +132 -0
  26. package/dist/detectIsolation-CrV8IFDv.mjs +339 -0
  27. package/dist/envFlag-COOuJ3AH.mjs +11 -0
  28. package/dist/fileLock-BMPwCrB6.mjs +80 -0
  29. package/dist/{fleet-BHzlAvnu.mjs → fleet-Ca-UjJNL.mjs} +122 -36
  30. package/dist/flushExit-C-i6SGjj.mjs +37 -0
  31. package/dist/friendlyName-1UZcyDhu.mjs +586 -0
  32. package/dist/{headlessCli-C2gvOpOt.mjs → headlessCli-C8HzQvis.mjs} +3 -21
  33. package/dist/hookSettings-FFc1pi8j.mjs +10546 -0
  34. package/dist/{httpServer-Be1Hcddr.mjs → httpServer-BzEDbOGn.mjs} +14 -5
  35. package/dist/hyphaClient-ybivOjZo.mjs +49 -0
  36. package/dist/index.mjs +40 -8
  37. package/dist/instance-CeHz5C8v.mjs +203 -0
  38. package/dist/kimiProvider-DbxODebR.mjs +69 -0
  39. package/dist/kimiSetup-NGsoYB5h.mjs +48 -0
  40. package/dist/loopVerify-DALtTk2V.mjs +444 -0
  41. package/dist/machineTools-DygM1jXW.mjs +580 -0
  42. package/dist/model-CUHojegn.mjs +66 -0
  43. package/dist/{notifyCommands-0aZUwKQK.mjs → notifyCommands-CCUE18Q-.mjs} +3 -18
  44. package/dist/package-BrsiNutS.mjs +64 -0
  45. package/dist/pinnedCodex-XW4qvOAe.mjs +11 -0
  46. package/dist/pinnedNono-5ZHb6328.mjs +15 -0
  47. package/dist/{rpc-rAQQ1q8t.mjs → rpc-CBkSkzsI.mjs} +38 -6
  48. package/dist/{rpc-B2Lc2BAB.mjs → rpc-ccY7xvlo.mjs} +33 -22
  49. package/dist/{run-C7QVfyw4.mjs → run-DCq3aEiF.mjs} +17 -9
  50. package/dist/run-DDmeV9K3.mjs +9826 -0
  51. package/dist/sandboxDetect-Cdzsj5TC.mjs +19 -0
  52. package/dist/{scheduler-CzF4lOe_.mjs → scheduler-BkVJ27Ki.mjs} +38 -6
  53. package/dist/securityContext-Cm5UTCuD.mjs +218 -0
  54. package/dist/serveAuth-4M0vwMyj.mjs +460 -0
  55. package/dist/{serveCommands-EIEqdaVu.mjs → serveCommands-CLhkccJM.mjs} +13 -34
  56. package/dist/sharingNotificationSync-qGIQ-3yf.mjs +359 -0
  57. package/dist/{sideband-BoutiRJi.mjs → sideband-68HhGT0i.mjs} +1 -21
  58. package/dist/store-B4E6NzG6.mjs +356 -0
  59. package/dist/store-fi0f-ff1.mjs +184 -0
  60. package/dist/types-57J3Re0n.mjs +17 -0
  61. package/dist/wantsHelp-6bMOeNUz.mjs +5 -0
  62. package/dist/wrapCommand-CdNawSaa.mjs +117 -0
  63. package/package.json +3 -3
  64. package/dist/cli-B24CmIli.mjs +0 -2780
  65. package/dist/package-CTbGYaJG.mjs +0 -64
  66. package/dist/run-CF4kAVPW.mjs +0 -26508
  67. package/dist/sandboxDetect-DNTcbgWD.mjs +0 -12
@@ -1,27 +1,5 @@
1
1
  import * as path from 'path';
2
- import { w as wantsHelp } from './cli-B24CmIli.mjs';
3
- import './run-CF4kAVPW.mjs';
4
- import 'os';
5
- import 'fs/promises';
6
- import 'fs';
7
- import 'url';
8
- import 'child_process';
9
- import 'crypto';
10
- import 'node:fs';
11
- import 'node:path';
12
- import 'node:crypto';
13
- import 'node:child_process';
14
- import 'util';
15
- import 'http';
16
- import 'net';
17
- import 'node:os';
18
- import 'node:events';
19
- import '@agentclientprotocol/sdk';
20
- import 'node:util';
21
- import 'node:readline';
22
- import 'node:fs/promises';
23
- import 'yaml';
24
- import './serviceManager-hlOVxkhW.mjs';
2
+ import { w as wantsHelp } from './wantsHelp-6bMOeNUz.mjs';
25
3
 
26
4
  function getFlag(args, flag) {
27
5
  const idx = args.indexOf(flag);
@@ -83,7 +61,7 @@ async function serviceExpose(args) {
83
61
  process.exit(1);
84
62
  }
85
63
  if (foreground) {
86
- const { runFrpcTunnel } = await import('./run-CF4kAVPW.mjs').then(function (n) { return n.as; });
64
+ const { runFrpcTunnel } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
87
65
  await runFrpcTunnel(name, ports, void 0, {
88
66
  group,
89
67
  groupKey,
@@ -93,7 +71,7 @@ async function serviceExpose(args) {
93
71
  });
94
72
  return;
95
73
  }
96
- const { connectAndGetMachine } = await import('./commands-DDuGBSsz.mjs');
74
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
97
75
  const { server, machine } = await connectAndGetMachine();
98
76
  try {
99
77
  const status = await machine.tunnelStart({
@@ -115,7 +93,7 @@ async function serviceExpose(args) {
115
93
  console.log(` port ${port}: ${url}`);
116
94
  }
117
95
  if (process.env.SVAMP_SESSION_ID) {
118
- const { autoAddSessionLink } = await import('./agentCommands-BuO988tJ.mjs');
96
+ const { autoAddSessionLink } = await import('./agentCommands-D91RcjqQ.mjs');
119
97
  let added = 0;
120
98
  for (const [port, url] of urlEntries) {
121
99
  const label = urlEntries.length > 1 ? `${name}:${port}` : name;
@@ -151,8 +129,8 @@ async function serviceServe(args) {
151
129
  const resolvedDir = path.resolve(directory);
152
130
  console.log(`Serving ${resolvedDir}`);
153
131
  const http = await import('http');
154
- const { serveStaticMount } = await import('./run-CF4kAVPW.mjs').then(function (n) { return n.at; });
155
- const { getFreePort } = await import('./run-CF4kAVPW.mjs').then(function (n) { return n.as; });
132
+ const { serveStaticMount } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.x; });
133
+ const { getFreePort } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
156
134
  const server = http.createServer((req, res) => {
157
135
  const u = new URL(req.url || "/", "http://127.0.0.1");
158
136
  let rel = u.pathname;
@@ -175,7 +153,7 @@ async function serviceServe(args) {
175
153
  resolve(18080);
176
154
  });
177
155
  });
178
- const { runFrpcTunnel } = await import('./run-CF4kAVPW.mjs').then(function (n) { return n.as; });
156
+ const { runFrpcTunnel } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
179
157
  void server;
180
158
  await runFrpcTunnel(name, [servePort]);
181
159
  } catch (err) {
@@ -185,7 +163,7 @@ async function serviceServe(args) {
185
163
  }
186
164
  async function serviceList(_args) {
187
165
  try {
188
- const { connectAndGetMachine } = await import('./commands-DDuGBSsz.mjs');
166
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
189
167
  const { server, machine } = await connectAndGetMachine();
190
168
  try {
191
169
  const tunnels = await machine.tunnelList({});
@@ -225,11 +203,11 @@ async function serviceDelete(args) {
225
203
  process.exit(1);
226
204
  }
227
205
  try {
228
- const { connectAndGetMachine } = await import('./commands-DDuGBSsz.mjs');
206
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
229
207
  const { server, machine } = await connectAndGetMachine();
230
208
  try {
231
209
  await machine.tunnelStop({ name });
232
- const { removeSessionLinkByService } = await import('./agentCommands-BuO988tJ.mjs');
210
+ const { removeSessionLinkByService } = await import('./agentCommands-D91RcjqQ.mjs');
233
211
  removeSessionLinkByService("tunnel", name);
234
212
  console.log(`Stopped tunnel '${name}'.`);
235
213
  } catch (err) {
@@ -0,0 +1,132 @@
1
+ const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
2
+ const MONTH_NAMES = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
3
+ const DOW_NAMES = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
4
+ function substituteNames(token, fieldIndex) {
5
+ const names = fieldIndex === 3 ? MONTH_NAMES : fieldIndex === 4 ? DOW_NAMES : null;
6
+ if (!names) return token;
7
+ return token.replace(/[A-Za-z]+/g, (word) => {
8
+ const i = names.indexOf(word.toUpperCase());
9
+ if (i < 0) return word;
10
+ return String(fieldIndex === 3 ? i + 1 : i);
11
+ });
12
+ }
13
+ function normalizeDow(set) {
14
+ if (!set.has(7)) return set;
15
+ const out = new Set(set);
16
+ out.delete(7);
17
+ out.add(0);
18
+ return out;
19
+ }
20
+ function parseField(token, [min, max]) {
21
+ const set = /* @__PURE__ */ new Set();
22
+ for (const part of token.split(",")) {
23
+ let m;
24
+ if (part === "*") {
25
+ for (let i = min; i <= max; i++) set.add(i);
26
+ } else if (m = part.match(/^\*\/(\d+)$/)) {
27
+ const s = +m[1];
28
+ if (s <= 0) throw new Error(`invalid cron step (must be >= 1): "${token}"`);
29
+ for (let i = min; i <= max; i += s) set.add(i);
30
+ } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
31
+ const s = +m[3];
32
+ if (s <= 0) throw new Error(`invalid cron step (must be >= 1): "${token}"`);
33
+ if (+m[1] > +m[2]) throw new Error(`invalid cron range (start > end): "${token}"`);
34
+ for (let i = +m[1]; i <= +m[2]; i += s) set.add(i);
35
+ } else if (m = part.match(/^(\d+)-(\d+)$/)) {
36
+ if (+m[1] > +m[2]) throw new Error(`invalid cron range (start > end): "${token}"`);
37
+ for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
38
+ } else if (m = part.match(/^(\d+)$/)) {
39
+ set.add(+m[1]);
40
+ } else throw new Error(`invalid cron field: "${token}"`);
41
+ }
42
+ for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
43
+ return set;
44
+ }
45
+ function parseCron(expr) {
46
+ const fields = String(expr).trim().split(/\s+/);
47
+ if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
48
+ const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(substituteNames(f, i), FIELD_RANGES[i]));
49
+ return { minute, hour, dom, month, dow: normalizeDow(dow), domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
50
+ }
51
+ function validateCronExpr(expr) {
52
+ parseCron(expr);
53
+ }
54
+ function cronMatches(expr, date) {
55
+ const c = typeof expr === "string" ? parseCron(expr) : expr;
56
+ if (!c.minute.has(date.getMinutes())) return false;
57
+ if (!c.hour.has(date.getHours())) return false;
58
+ if (!c.month.has(date.getMonth() + 1)) return false;
59
+ const domOk = c.dom.has(date.getDate());
60
+ const dowOk = c.dow.has(date.getDay());
61
+ if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
62
+ return domOk && dowOk;
63
+ }
64
+ const _zoneFormatters = /* @__PURE__ */ new Map();
65
+ function zoneFormatter(tz) {
66
+ let f = _zoneFormatters.get(tz);
67
+ if (!f) {
68
+ f = new Intl.DateTimeFormat("en-US", {
69
+ timeZone: tz,
70
+ hour12: false,
71
+ year: "numeric",
72
+ month: "2-digit",
73
+ day: "2-digit",
74
+ hour: "2-digit",
75
+ minute: "2-digit"
76
+ });
77
+ _zoneFormatters.set(tz, f);
78
+ }
79
+ return f;
80
+ }
81
+ function inZone(date, tz) {
82
+ if (!tz) return date;
83
+ try {
84
+ const p = zoneFormatter(tz).formatToParts(date).reduce((o, x) => {
85
+ o[x.type] = x.value;
86
+ return o;
87
+ }, {});
88
+ const midnight24 = p.hour === "24";
89
+ return new Date(+p.year, +p.month - 1, +p.day + (midnight24 ? 1 : 0), midnight24 ? 0 : +p.hour, +p.minute);
90
+ } catch {
91
+ return date;
92
+ }
93
+ }
94
+ function nextFire(expr, from, tz) {
95
+ const c = parseCron(expr);
96
+ if (tz && scanForMatch(c, from, void 0) === null) return null;
97
+ return scanForMatch(c, from, tz);
98
+ }
99
+ function scanForMatch(c, from, tz) {
100
+ const d = new Date(from.getTime());
101
+ d.setSeconds(0, 0);
102
+ d.setMinutes(d.getMinutes() + 1);
103
+ for (let i = 0; i < 366 * 24 * 60; i++) {
104
+ if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
105
+ d.setMinutes(d.getMinutes() + 1);
106
+ }
107
+ return null;
108
+ }
109
+ function describeNextFire(expr, tz, from = /* @__PURE__ */ new Date()) {
110
+ let next;
111
+ try {
112
+ next = nextFire(expr, from, tz);
113
+ } catch {
114
+ return null;
115
+ }
116
+ if (!next) return "never fires";
117
+ const shown = tz ? inZone(next, tz) : next;
118
+ const pad = (n) => String(n).padStart(2, "0");
119
+ const stamp = `${shown.getFullYear()}-${pad(shown.getMonth() + 1)}-${pad(shown.getDate())} ${pad(shown.getHours())}:${pad(shown.getMinutes())}`;
120
+ return `next ${stamp} ${tz || "local"}`;
121
+ }
122
+ function resolvePath(ctx, path) {
123
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
124
+ }
125
+ function renderTemplate(template, ctx) {
126
+ return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
127
+ const v = resolvePath(ctx, p);
128
+ return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
129
+ });
130
+ }
131
+
132
+ export { cronMatches as c, describeNextFire as d, inZone as i, renderTemplate as r, validateCronExpr as v };
@@ -0,0 +1,339 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, rm, readFile, mkdtemp, access, chmod, rename } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { createHash } from 'node:crypto';
7
+ import { P as PINNED_NONO_VERSION, n as nonoAssetUrl, N as NONO_CHECKSUM_ASSET, e as expectedSha256 } from './pinnedNono-5ZHb6328.mjs';
8
+ import { I as ISOLATION_PREFERENCE } from './types-57J3Re0n.mjs';
9
+ import { e as envFlagEnabled } from './envFlag-COOuJ3AH.mjs';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const VERIFY_HARD_TIMEOUT_MS = 2e4;
13
+ async function verifyWithHardTimeout(method, binaryPath, timeoutMs = VERIFY_HARD_TIMEOUT_MS) {
14
+ let timer;
15
+ try {
16
+ return await Promise.race([
17
+ verifyIsolation(method, binaryPath),
18
+ new Promise((resolve) => {
19
+ timer = setTimeout(
20
+ () => resolve({
21
+ passed: false,
22
+ error: `verification timed out after ${timeoutMs}ms (runtime unresponsive)`
23
+ }),
24
+ timeoutMs
25
+ );
26
+ })
27
+ ]);
28
+ } finally {
29
+ if (timer) clearTimeout(timer);
30
+ }
31
+ }
32
+ const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
33
+ const SVAMP_BIN_DIR = join(SVAMP_TOOLS_DIR, "bin");
34
+ async function checkCommand(command, versionArgs) {
35
+ try {
36
+ const { stdout } = await execFileAsync(command, versionArgs, {
37
+ timeout: 5e3
38
+ });
39
+ const version = stdout.trim().split("\n")[0];
40
+ return { found: true, version, path: command };
41
+ } catch {
42
+ }
43
+ const localPath = join(SVAMP_BIN_DIR, command);
44
+ try {
45
+ const { stdout } = await execFileAsync(localPath, versionArgs, {
46
+ timeout: 5e3
47
+ });
48
+ const version = stdout.trim().split("\n")[0];
49
+ return { found: true, version, path: localPath };
50
+ } catch {
51
+ }
52
+ return { found: false };
53
+ }
54
+ function getNonoAssetName(version) {
55
+ const platform = process.platform;
56
+ const arch = process.arch;
57
+ if (platform === "darwin" && arch === "arm64") {
58
+ return `nono-v${version}-aarch64-apple-darwin.tar.gz`;
59
+ }
60
+ if (platform === "darwin" && arch === "x64") {
61
+ return `nono-v${version}-x86_64-apple-darwin.tar.gz`;
62
+ }
63
+ if (platform === "linux" && arch === "x64") {
64
+ return `nono-v${version}-x86_64-unknown-linux-gnu.tar.gz`;
65
+ }
66
+ return null;
67
+ }
68
+ async function probeNonoRuns(binaryPath) {
69
+ try {
70
+ const { stdout } = await execFileAsync(binaryPath, ["--version"], { timeout: 5e3 });
71
+ return { ok: true, version: stdout.trim().split("\n")[0] };
72
+ } catch (e) {
73
+ const stderr = typeof e?.stderr === "string" ? e.stderr.trim() : "";
74
+ const first = stderr.split("\n").find((l) => l.trim()) || e?.message || "unknown error";
75
+ return { ok: false, reason: first.trim() };
76
+ }
77
+ }
78
+ async function installNono() {
79
+ const platform = process.platform;
80
+ if (platform !== "darwin" && platform !== "linux") {
81
+ return false;
82
+ }
83
+ console.log(`[isolation] Installing pinned nono ${PINNED_NONO_VERSION}...`);
84
+ try {
85
+ const version = PINNED_NONO_VERSION;
86
+ const assetName = getNonoAssetName(version);
87
+ if (!assetName) {
88
+ console.warn(`[isolation] No nono binary for ${platform}/${process.arch}`);
89
+ return false;
90
+ }
91
+ const downloadUrl = nonoAssetUrl(version, assetName);
92
+ console.log(`[isolation] Downloading nono ${version} (pinned) from ${downloadUrl}...`);
93
+ await mkdir(SVAMP_BIN_DIR, { recursive: true });
94
+ const tarball = join(SVAMP_BIN_DIR, assetName);
95
+ await execFileAsync("curl", [
96
+ "-fsSL",
97
+ "-o",
98
+ tarball,
99
+ "--connect-timeout",
100
+ "10",
101
+ downloadUrl
102
+ ], { timeout: 6e4 });
103
+ let expected = null;
104
+ try {
105
+ const { stdout: sums } = await execFileAsync("curl", [
106
+ "-fsSL",
107
+ "--connect-timeout",
108
+ "10",
109
+ nonoAssetUrl(version, NONO_CHECKSUM_ASSET)
110
+ ], { timeout: 2e4, maxBuffer: 4 * 1024 * 1024 });
111
+ expected = expectedSha256(sums, assetName);
112
+ } catch {
113
+ expected = null;
114
+ }
115
+ if (!expected) {
116
+ console.error(`[isolation] Could not obtain a published SHA-256 for ${assetName} \u2014 refusing to install nono.`);
117
+ await rm(tarball, { force: true }).catch(() => {
118
+ });
119
+ return false;
120
+ }
121
+ const actual = createHash("sha256").update(await readFile(tarball)).digest("hex");
122
+ if (actual !== expected) {
123
+ console.error(`[isolation] nono checksum MISMATCH for ${assetName}: expected ${expected}, got ${actual}. Refusing to install.`);
124
+ await rm(tarball, { force: true }).catch(() => {
125
+ });
126
+ return false;
127
+ }
128
+ console.log(`[isolation] nono ${version} checksum verified (${actual.slice(0, 12)}\u2026)`);
129
+ const nonoPath = join(SVAMP_BIN_DIR, "nono");
130
+ const stageDir = await mkdtemp(join(SVAMP_BIN_DIR, ".nono-stage-"));
131
+ try {
132
+ await execFileAsync("tar", ["xzf", tarball, "-C", stageDir], { timeout: 15e3 });
133
+ await rm(tarball, { force: true }).catch(() => {
134
+ });
135
+ const staged = join(stageDir, "nono");
136
+ try {
137
+ await access(staged);
138
+ } catch {
139
+ console.warn("[isolation] Download completed but nono binary not found in the archive");
140
+ return false;
141
+ }
142
+ await chmod(staged, 493);
143
+ const probe = await probeNonoRuns(staged);
144
+ if (!probe.ok) {
145
+ console.error(
146
+ `[isolation] nono ${version} downloaded and checksum-verified, but it CANNOT RUN on this host: ${probe.reason}. Refusing to install \u2014 the previous binary (if any) is untouched. This host needs a nono build compatible with its libc; sessions will fall back to docker/podman, where --allow-write and --allow-domain are NOT enforced (#0878).`
147
+ );
148
+ return false;
149
+ }
150
+ await rename(staged, nonoPath);
151
+ console.log(`[isolation] nono ${version} installed at ${nonoPath} (verified runnable: ${probe.version || "ok"})`);
152
+ return true;
153
+ } finally {
154
+ await rm(stageDir, { recursive: true, force: true }).catch(() => {
155
+ });
156
+ }
157
+ } catch (e) {
158
+ console.warn(`[isolation] Failed to install nono: ${e.message}`);
159
+ return false;
160
+ }
161
+ }
162
+ async function parseIsolationTestOutput(stdout, probeFile) {
163
+ const output = stdout.trim();
164
+ const wMatch = output.match(/W=(\d+)/);
165
+ const pMatch = output.match(/P=(\d+)/);
166
+ if (!wMatch || !pMatch) {
167
+ return { passed: false, error: `unexpected test output: ${output}` };
168
+ }
169
+ const workspaceWriteOk = wMatch[1] === "0";
170
+ const probeExitCode = parseInt(pMatch[1], 10);
171
+ if (!workspaceWriteOk) {
172
+ return { passed: false, error: "sandbox blocked writes to allowed workspace path" };
173
+ }
174
+ if (probeExitCode === 0) {
175
+ try {
176
+ await access(probeFile);
177
+ return {
178
+ passed: false,
179
+ error: "writes outside workspace were NOT blocked \u2014 file leaked to host filesystem"
180
+ };
181
+ } catch {
182
+ return { passed: true };
183
+ }
184
+ }
185
+ return { passed: true };
186
+ }
187
+ async function verifyNonoIsolation(binaryPath) {
188
+ const testBase = "/tmp";
189
+ const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
190
+ const probeFile = join(homedir(), `.svamp-iso-probe-${process.pid}`);
191
+ try {
192
+ const testScript = [
193
+ `echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
194
+ `echo leak > "${probeFile}" 2>/dev/null; P=$?`,
195
+ `echo "W=$W P=$P"`
196
+ ].join("; ");
197
+ const { stdout } = await execFileAsync(binaryPath, [
198
+ "run",
199
+ "-s",
200
+ "--allow",
201
+ workDir,
202
+ // NOTE: Do NOT add --allow-cwd here. If the daemon's CWD happens to be
203
+ // $HOME (common when started interactively), --allow-cwd would grant
204
+ // access to $HOME, allowing the probe file write to succeed and making
205
+ // verification incorrectly fail ("file leaked to host filesystem").
206
+ // We already grant --allow workDir explicitly, so --allow-cwd is redundant.
207
+ "--trust-override",
208
+ "--",
209
+ "sh",
210
+ "-c",
211
+ testScript
212
+ ], { timeout: 15e3, cwd: workDir });
213
+ return parseIsolationTestOutput(stdout, probeFile);
214
+ } catch (e) {
215
+ return { passed: false, error: e.message };
216
+ } finally {
217
+ await rm(workDir, { recursive: true, force: true }).catch(() => {
218
+ });
219
+ await rm(probeFile, { force: true }).catch(() => {
220
+ });
221
+ }
222
+ }
223
+ async function verifyContainerIsolation(method, binaryPath) {
224
+ try {
225
+ await execFileAsync(binaryPath, ["info"], { timeout: 15e3, killSignal: "SIGKILL" });
226
+ } catch (e) {
227
+ return { passed: false, error: `${method} daemon not accessible: ${e.message}` };
228
+ }
229
+ try {
230
+ const { stdout } = await execFileAsync(
231
+ binaryPath,
232
+ ["run", "--rm", "--network=none", "alpine", "echo", "isolation-ok"],
233
+ { timeout: 3e4, killSignal: "SIGKILL" }
234
+ );
235
+ if (!stdout.includes("isolation-ok")) {
236
+ return { passed: false, error: `container test returned unexpected output: ${stdout.trim()}` };
237
+ }
238
+ return { passed: true };
239
+ } catch (e) {
240
+ if (e.message?.includes("pull access denied") || e.message?.includes("not found")) {
241
+ return { passed: true };
242
+ }
243
+ return { passed: false, error: `container test failed: ${e.message}` };
244
+ }
245
+ }
246
+ async function verifyIsolation(method, binaryPath) {
247
+ switch (method) {
248
+ case "nono":
249
+ return verifyNonoIsolation(binaryPath);
250
+ case "docker":
251
+ case "podman":
252
+ return verifyContainerIsolation(method, binaryPath);
253
+ default:
254
+ return { passed: false, error: `${method} is not supported for external isolation` };
255
+ }
256
+ }
257
+ function isolationDisabledByEnv(env = process.env) {
258
+ const v = (env.SVAMP_ISOLATION || "").trim().toLowerCase();
259
+ if (v && ["none", "off", "disabled", "0", "false", "no"].includes(v)) return true;
260
+ return envFlagEnabled(env.SVAMP_NO_ISOLATION) || envFlagEnabled(env.SVAMP_SKIP_NONO);
261
+ }
262
+ async function detectIsolationCapabilities() {
263
+ if (isolationDisabledByEnv()) {
264
+ console.log(
265
+ "[isolation] Disabled via env (SVAMP_ISOLATION=none) \u2014 skipping nono auto-install + runtime detection. Sessions run WITHOUT per-session sandboxing; session sharing is allowed (rely on the container/host boundary)."
266
+ );
267
+ const notFound = () => ({ found: false });
268
+ return {
269
+ available: [],
270
+ preferred: null,
271
+ disabled: true,
272
+ details: {
273
+ nono: notFound(),
274
+ docker: notFound(),
275
+ podman: notFound(),
276
+ srt: notFound(),
277
+ bwrap: notFound()
278
+ }
279
+ };
280
+ }
281
+ const checks = {
282
+ nono: checkCommand("nono", ["--version"]),
283
+ docker: checkCommand("docker", ["--version"]),
284
+ podman: checkCommand("podman", ["--version"])
285
+ };
286
+ const checkResults = await Promise.all(
287
+ Object.entries(checks).map(async ([method, promise]) => {
288
+ const detail = await promise;
289
+ return [method, detail];
290
+ })
291
+ );
292
+ const details = {
293
+ ...Object.fromEntries(checkResults),
294
+ // Include legacy types in details for backward compat
295
+ srt: { found: false },
296
+ bwrap: { found: false }
297
+ };
298
+ if (!details.nono.found) {
299
+ const installed = await installNono();
300
+ if (installed) {
301
+ details.nono = await checkCommand("nono", ["--version"]);
302
+ }
303
+ }
304
+ const foundMethods = ISOLATION_PREFERENCE.filter((m) => details[m].found);
305
+ if (foundMethods.length > 0) {
306
+ console.log(`[isolation] Found runtimes: ${foundMethods.join(", ")}. Verifying isolation...`);
307
+ await Promise.all(
308
+ foundMethods.map(async (method) => {
309
+ const binaryPath = details[method].path || method;
310
+ const result = await verifyWithHardTimeout(method, binaryPath);
311
+ details[method].verified = result.passed;
312
+ if (!result.passed) {
313
+ details[method].verificationError = result.error;
314
+ console.warn(
315
+ `[isolation] WARNING: ${method} found but verification FAILED: ${result.error}. This runtime will NOT be used for isolation.`
316
+ );
317
+ } else {
318
+ console.log(`[isolation] ${method}: verified OK`);
319
+ }
320
+ })
321
+ );
322
+ }
323
+ const available = ISOLATION_PREFERENCE.filter(
324
+ (m) => details[m].found && details[m].verified === true
325
+ );
326
+ const preferred = available.length > 0 ? available[0] : null;
327
+ if (available.length === 0 && foundMethods.length > 0) {
328
+ console.warn(
329
+ `[isolation] No isolation runtime passed verification (found: ${foundMethods.join(", ")}). Session sharing will be DISABLED.`
330
+ );
331
+ } else if (available.length === 0) {
332
+ console.log("[isolation] No isolation runtimes found. Session sharing will be unavailable.");
333
+ } else {
334
+ console.log(`[isolation] Preferred isolation method: ${preferred}`);
335
+ }
336
+ return { available, preferred, details };
337
+ }
338
+
339
+ export { detectIsolationCapabilities, installNono, isolationDisabledByEnv, probeNonoRuns, verifyWithHardTimeout };
@@ -0,0 +1,11 @@
1
+ const NEGATIVE = /* @__PURE__ */ new Set(["", "0", "false", "no", "off"]);
2
+ function envFlagEnabled(value) {
3
+ if (value == null) return false;
4
+ return !NEGATIVE.has(value.trim().toLowerCase());
5
+ }
6
+ function envFlagDisabled(value) {
7
+ if (value == null) return false;
8
+ return NEGATIVE.has(value.trim().toLowerCase());
9
+ }
10
+
11
+ export { envFlagDisabled as a, envFlagEnabled as e };
@@ -0,0 +1,80 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, rmSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const _sleepSab = new Int32Array(new SharedArrayBuffer(4));
5
+ function sleepSync(ms) {
6
+ try {
7
+ Atomics.wait(_sleepSab, 0, 0, ms);
8
+ } catch {
9
+ }
10
+ }
11
+ function isProcessDead(pid) {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return false;
15
+ } catch (e) {
16
+ return e?.code === "ESRCH";
17
+ }
18
+ }
19
+ function withFileLock(lockPath, fn, opts) {
20
+ const deadlineMs = opts?.deadlineMs ?? 50;
21
+ const staleMs = opts?.staleMs ?? 5e3;
22
+ const trackOwner = opts?.breakOnDeadOwner ?? false;
23
+ const pidPath = join(lockPath, "owner.pid");
24
+ const deadline = Date.now() + deadlineMs;
25
+ let held = false;
26
+ while (Date.now() < deadline) {
27
+ try {
28
+ mkdirSync(lockPath);
29
+ held = true;
30
+ if (trackOwner) {
31
+ try {
32
+ writeFileSync(pidPath, String(process.pid));
33
+ } catch {
34
+ }
35
+ }
36
+ break;
37
+ } catch {
38
+ let broke = false;
39
+ if (trackOwner) {
40
+ try {
41
+ const owner = Number(readFileSync(pidPath, "utf8"));
42
+ if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && isProcessDead(owner)) {
43
+ rmSync(lockPath, { recursive: true, force: true });
44
+ broke = true;
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ if (!broke) {
50
+ try {
51
+ if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
52
+ rmSync(lockPath, { recursive: true, force: true });
53
+ broke = true;
54
+ }
55
+ } catch {
56
+ }
57
+ }
58
+ if (broke) continue;
59
+ sleepSync(5);
60
+ }
61
+ }
62
+ if (!held) {
63
+ try {
64
+ opts?.onFailOpen?.(lockPath);
65
+ } catch {
66
+ }
67
+ }
68
+ try {
69
+ return fn();
70
+ } finally {
71
+ if (held) {
72
+ try {
73
+ rmSync(lockPath, { recursive: true, force: true });
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ export { sleepSync as s, withFileLock as w };