svamp-cli 0.2.195 → 0.2.197

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 (27) hide show
  1. package/bin/skills/loop/bin/stop-gate.mjs +31 -1
  2. package/bin/skills/loop/test/test-loop-gate.mjs +38 -0
  3. package/dist/{agentCommands-IdOiOg0f.mjs → agentCommands-Dxuc65V_.mjs} +5 -5
  4. package/dist/{auth-DmrnqTLo.mjs → auth-DweBQRAL.mjs} +1 -1
  5. package/dist/cli.mjs +60 -60
  6. package/dist/{commands-CbBOkn_G.mjs → commands-BF7fzTcc.mjs} +1 -1
  7. package/dist/{commands-DAYpMdgB.mjs → commands-BFZhHLhh.mjs} +2 -2
  8. package/dist/{commands-BlTV-jYX.mjs → commands-BItE-9OU.mjs} +1 -1
  9. package/dist/{commands-DHI31wAh.mjs → commands-BJ3G0VVJ.mjs} +1 -1
  10. package/dist/{commands-DZhCJXnB.mjs → commands-BWD_a3hE.mjs} +2 -2
  11. package/dist/{commands-B3uk2hcj.mjs → commands-Bxb8oEBa.mjs} +1 -1
  12. package/dist/{commands-DjT7M2al.mjs → commands-C1V3SEF9.mjs} +5 -5
  13. package/dist/{fleet-BtwsNKuT.mjs → fleet-C-2EKVda.mjs} +1 -1
  14. package/dist/{frpc-WpRu5zE_.mjs → frpc-BgabHfqV.mjs} +1 -1
  15. package/dist/{headlessCli-Dy049YdV.mjs → headlessCli-CtTe4oJ4.mjs} +2 -2
  16. package/dist/{httpServer-Cf54pQ77.mjs → httpServer-B1KVQJfm.mjs} +38 -0
  17. package/dist/index.mjs +1 -1
  18. package/dist/{package-ARvCEkBY.mjs → package-PTsbpBGF.mjs} +2 -2
  19. package/dist/{rpc-DgV6_G_Q.mjs → rpc-C35Y8Vto.mjs} +1 -1
  20. package/dist/{rpc-CvpwmghB.mjs → rpc-DlM7ml1e.mjs} +1 -1
  21. package/dist/{run-DiCtjIs5.mjs → run-B3fosGEs.mjs} +335 -36
  22. package/dist/{run-D79u_Ud4.mjs → run-DtTnHqcs.mjs} +1 -1
  23. package/dist/{scheduler-D8Xj2_vB.mjs → scheduler-B3hBIkt8.mjs} +1 -1
  24. package/dist/{serveCommands-Bc9DXaQv.mjs → serveCommands-Bj2aEfKX.mjs} +5 -5
  25. package/dist/{serveManager-C2ykbnxd.mjs → serveManager-CCslS9Yt.mjs} +2 -2
  26. package/dist/{sideband-B9uONcBg.mjs → sideband-BtZMx9Af.mjs} +1 -1
  27. package/package.json +2 -2
@@ -1,15 +1,15 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir as homedir$1 } from 'os';
2
2
  import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
- import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, realpathSync, rmSync as rmSync$1, copyFileSync, unlinkSync as unlinkSync$1, readdirSync as readdirSync$1, watch, rmdirSync } from 'fs';
4
- import path__default, { join as join$1, resolve, dirname as dirname$1, basename as basename$1 } from 'path';
3
+ import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, copyFileSync, unlinkSync as unlinkSync$1, readdirSync as readdirSync$1, watch, rmdirSync } from 'fs';
4
+ import path__default, { join as join$1, resolve as resolve$1, dirname as dirname$1, basename as basename$1 } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { randomBytes, createHash, randomUUID } from 'node:crypto';
9
- import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, renameSync, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
+ import { existsSync, readFileSync, realpathSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync, rmSync, appendFileSync, unlinkSync } from 'node:fs';
10
10
  import { exec, execSync, spawn, execFile as execFile$1, execFileSync } from 'node:child_process';
11
11
  import { promisify } from 'util';
12
- import { join, basename, dirname } from 'node:path';
12
+ import { extname, resolve, join, sep, basename, dirname } from 'node:path';
13
13
  import { EventEmitter } from 'node:events';
14
14
  import os, { homedir, platform } from 'node:os';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -1233,6 +1233,155 @@ function renderTemplate(template, ctx) {
1233
1233
  });
1234
1234
  }
1235
1235
 
1236
+ const HARD_MAX_BYTES = 25 * 1024 * 1024;
1237
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
1238
+ const DEFAULT_MAX_COUNT = 100;
1239
+ const DEFAULT_ALLOWED_EXT = [
1240
+ ".png",
1241
+ ".jpg",
1242
+ ".jpeg",
1243
+ ".gif",
1244
+ ".webp",
1245
+ ".bmp",
1246
+ ".svg",
1247
+ ".pdf",
1248
+ ".txt",
1249
+ ".md",
1250
+ ".csv",
1251
+ ".json",
1252
+ ".log",
1253
+ ".yaml",
1254
+ ".yml"
1255
+ ];
1256
+ function uploadEnabled(c) {
1257
+ return !!(c && c.upload && c.upload.enabled === true);
1258
+ }
1259
+ function effectiveUploadConfig(c) {
1260
+ const u = c.upload || {};
1261
+ const maxBytes = Math.min(
1262
+ typeof u.maxBytes === "number" && u.maxBytes > 0 ? u.maxBytes : DEFAULT_MAX_BYTES,
1263
+ HARD_MAX_BYTES
1264
+ );
1265
+ const maxCount = typeof u.maxCount === "number" && u.maxCount > 0 ? Math.floor(u.maxCount) : DEFAULT_MAX_COUNT;
1266
+ const allowedExt = Array.isArray(u.allowedExt) ? u.allowedExt.map((e) => String(e).toLowerCase().trim()).filter(Boolean) : DEFAULT_ALLOWED_EXT;
1267
+ return { ...u, maxBytes, maxCount, allowedExt };
1268
+ }
1269
+ function validateUploadConfig(c) {
1270
+ const u = c.upload;
1271
+ if (u == null) return [];
1272
+ if (typeof u !== "object") return ["upload must be an object"];
1273
+ const errs = [];
1274
+ if (u.enabled != null && typeof u.enabled !== "boolean") errs.push("upload.enabled must be a boolean");
1275
+ if (u.maxBytes != null && (typeof u.maxBytes !== "number" || u.maxBytes <= 0)) errs.push("upload.maxBytes must be a positive number");
1276
+ if (u.maxCount != null && (typeof u.maxCount !== "number" || u.maxCount <= 0)) errs.push("upload.maxCount must be a positive number");
1277
+ if (u.allowedExt != null && !Array.isArray(u.allowedExt)) errs.push("upload.allowedExt must be an array of extensions");
1278
+ if (u.allowedMime != null && !Array.isArray(u.allowedMime)) errs.push("upload.allowedMime must be an array of mime types");
1279
+ if (u.dir != null) {
1280
+ if (typeof u.dir !== "string") errs.push("upload.dir must be a string");
1281
+ else if (u.dir.startsWith("/") || u.dir.startsWith("\\") || /(^|[\\/])\.\.([\\/]|$)/.test(u.dir) || u.dir.includes("\0"))
1282
+ errs.push('upload.dir must be a project-relative path without ".." traversal');
1283
+ }
1284
+ return errs;
1285
+ }
1286
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
1287
+ function sanitizeFilename(raw) {
1288
+ const input = String(raw ?? "").trim();
1289
+ if (!input) return { error: "filename required" };
1290
+ if (input.length > 200) return { error: "filename too long (max 200 chars)" };
1291
+ if (CONTROL_CHARS.test(input)) return { error: "filename contains control characters" };
1292
+ if (input.includes("\0")) return { error: "filename contains null byte" };
1293
+ if (input.includes("/") || input.includes("\\")) return { error: "filename must not contain path separators" };
1294
+ if (input.includes("..")) return { error: 'filename must not contain ".."' };
1295
+ const name = basename(input);
1296
+ if (!name || name === "." || name === "..") return { error: "invalid filename" };
1297
+ if (name.startsWith(".")) return { error: "filename must not start with a dot" };
1298
+ if (!/^[A-Za-z0-9 ._-]+$/.test(name)) return { error: "filename has disallowed characters (use letters, digits, space, . _ -)" };
1299
+ return { name };
1300
+ }
1301
+ function validateUpload(input) {
1302
+ const { projectDir, channel, bytes, contentType } = input;
1303
+ if (!uploadEnabled(channel)) return { error: "uploads are not enabled on this channel" };
1304
+ const cfg = effectiveUploadConfig(channel);
1305
+ const fn = sanitizeFilename(input.filename);
1306
+ if (fn.error || !fn.name) return { error: fn.error || "invalid filename" };
1307
+ const name = fn.name;
1308
+ if (!Number.isFinite(bytes) || bytes < 0) return { error: "invalid file size" };
1309
+ if (bytes === 0) return { error: "empty file" };
1310
+ if (bytes > cfg.maxBytes) return { error: `file too large (${bytes} bytes > ${cfg.maxBytes} limit)` };
1311
+ const ext = extname(name).toLowerCase();
1312
+ if (!ext) return { error: "file has no extension" };
1313
+ if (!cfg.allowedExt.includes(ext)) return { error: `file type not allowed: ${ext} (allowed: ${cfg.allowedExt.join(", ") || "none"})` };
1314
+ if (Array.isArray(cfg.allowedMime) && cfg.allowedMime.length) {
1315
+ const ct = String(contentType || "").toLowerCase().split(";")[0].trim();
1316
+ if (!ct) return { error: "content-type required (channel restricts MIME types)" };
1317
+ if (!cfg.allowedMime.map((m) => String(m).toLowerCase().trim()).includes(ct))
1318
+ return { error: `mime type not allowed: ${ct}` };
1319
+ }
1320
+ let projectReal;
1321
+ try {
1322
+ projectReal = realpathSync(projectDir);
1323
+ } catch {
1324
+ projectReal = resolve(projectDir);
1325
+ }
1326
+ const relDir = cfg.dir && String(cfg.dir).trim() ? String(cfg.dir).trim() : join(".svamp", "channels", "uploads", String(channel.id || "unknown"));
1327
+ const destDir = resolve(projectReal, relDir);
1328
+ if (destDir !== projectReal && !destDir.startsWith(projectReal + sep))
1329
+ return { error: "upload destination escapes the project folder" };
1330
+ const fullPath = join(destDir, name);
1331
+ if (fullPath !== destDir && !fullPath.startsWith(destDir + sep))
1332
+ return { error: "upload path escapes the destination directory" };
1333
+ try {
1334
+ if (existsSync(destDir)) {
1335
+ const count = readdirSync(destDir).filter((f) => {
1336
+ try {
1337
+ return statSync(join(destDir, f)).isFile();
1338
+ } catch {
1339
+ return false;
1340
+ }
1341
+ }).length;
1342
+ if (count >= cfg.maxCount) return { error: `upload limit reached (${cfg.maxCount} files)` };
1343
+ }
1344
+ } catch {
1345
+ }
1346
+ return { target: { destDir, name, fullPath } };
1347
+ }
1348
+ function writeUpload(projectDir, target, data) {
1349
+ mkdirSync(target.destDir, { recursive: true });
1350
+ let finalName = target.name;
1351
+ let finalPath = target.fullPath;
1352
+ if (existsSync(finalPath)) {
1353
+ const ext = extname(target.name);
1354
+ const stem = target.name.slice(0, target.name.length - ext.length);
1355
+ finalName = `${stem}-${randomBytes(3).toString("hex")}${ext}`;
1356
+ finalPath = join(target.destDir, finalName);
1357
+ }
1358
+ const tmp = finalPath + ".tmp-" + randomBytes(4).toString("hex");
1359
+ writeFileSync(tmp, data);
1360
+ renameSync(tmp, finalPath);
1361
+ let projectReal;
1362
+ try {
1363
+ projectReal = realpathSync(projectDir);
1364
+ } catch {
1365
+ projectReal = resolve(projectDir);
1366
+ }
1367
+ const relPath = finalPath.startsWith(projectReal + sep) ? finalPath.slice(projectReal.length + 1) : finalPath;
1368
+ return { name: finalName, relPath, fullPath: finalPath, bytes: data.length };
1369
+ }
1370
+ function decodeBase64Capped(b64, maxBytes) {
1371
+ const s = String(b64 ?? "");
1372
+ if (!s) return { error: "empty file content" };
1373
+ if (s.length > Math.ceil(maxBytes / 3) * 4 + 16) return { error: `file too large (exceeds ${maxBytes} byte limit)` };
1374
+ let data;
1375
+ try {
1376
+ data = Buffer.from(s, "base64");
1377
+ } catch {
1378
+ return { error: "invalid base64 content" };
1379
+ }
1380
+ if (!data.length) return { error: "invalid or empty base64 content" };
1381
+ if (data.length > maxBytes) return { error: `file too large (${data.length} bytes > ${maxBytes} limit)` };
1382
+ return { data };
1383
+ }
1384
+
1236
1385
  const genId = () => "c_" + randomBytes(5).toString("hex");
1237
1386
  const genKey = () => "ck_" + randomBytes(18).toString("base64url");
1238
1387
  const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
@@ -1268,6 +1417,7 @@ function validateChannel(c) {
1268
1417
  if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
1269
1418
  if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
1270
1419
  for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
1420
+ errs.push(...validateUploadConfig(c));
1271
1421
  return errs;
1272
1422
  }
1273
1423
  function normalizeBind(c) {
@@ -1377,6 +1527,7 @@ function generateSkillBody(channel, ctx) {
1377
1527
  const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
1378
1528
  const skillUrl = `${gw}/skill?channel=${channel.id}`;
1379
1529
  const sendUrl = `${gw}/send`;
1530
+ const uploadUrl = `${gw}/upload`;
1380
1531
  const key = ctx?.key || "<your-key>";
1381
1532
  const isAgent = channel.action?.kind === "agent";
1382
1533
  const isQueue = channel.reply?.mode === "queue";
@@ -1422,6 +1573,47 @@ to \`send()\`. The reply is then delivered straight to **your session's inbox**
1422
1573
  channel's outbox and you must poll \`receive()\` (above) for it \u2014 it will **not** appear
1423
1574
  in your inbox.` : "";
1424
1575
  const rpcAuthNote = hyphaOpen ? `Your verified Hypha identity is accepted \u2014 no key needed.` : `Uses your verified Hypha identity.`;
1576
+ const uploadSection = uploadEnabled(channel) ? (() => {
1577
+ const u = effectiveUploadConfig(channel);
1578
+ const mb = (u.maxBytes / (1024 * 1024)).toFixed(u.maxBytes % (1024 * 1024) === 0 ? 0 : 1);
1579
+ const exts = u.allowedExt.join(", ") || "(none)";
1580
+ const mimeLine = Array.isArray(u.allowedMime) && u.allowedMime.length ? `
1581
+ - **Allowed MIME types:** ${u.allowedMime.join(", ")}` : "";
1582
+ return `
1583
+
1584
+ ## Uploading a file
1585
+ This channel **accepts file uploads** (server-validated, deny-by-default elsewhere).
1586
+ Send the file base64-encoded; the server validates it and saves it into the session's
1587
+ project folder, returning the saved relative path. Reference that path in a follow-up
1588
+ \`send()\` so the agent knows what you uploaded.
1589
+
1590
+ **Limits (enforced server-side):**
1591
+ - **Max size:** ${mb} MiB per file
1592
+ - **Max files retained:** ${u.maxCount}
1593
+ - **Allowed extensions:** ${exts}${mimeLine}
1594
+ - Filenames are sanitized to a safe basename \u2014 no paths, no \`..\` traversal.
1595
+
1596
+ ### Hypha RPC
1597
+ \`\`\`js
1598
+ const res = await get_service("${svc}").upload({
1599
+ channel: "${channel.id}",
1600
+ filename: "report.pdf",
1601
+ content_base64: "<base64 of the file bytes>",
1602
+ content_type: "application/pdf",
1603
+ from: "your-name"${rSession ? `,
1604
+ session: "${rSession}"` : ""}${hyphaOpen ? "" : `,
1605
+ key: "${key}"`}
1606
+ });
1607
+ // \u2192 { ok: true, name, path, bytes }
1608
+ \`\`\`
1609
+
1610
+ ### HTTP
1611
+ \`\`\`bash
1612
+ curl -X POST "${uploadUrl}" \\
1613
+ -H 'Content-Type: application/json' \\
1614
+ -d '{"kwargs": {"channel": "${channel.id}", "filename": "report.pdf", "content_base64": "<base64>", "content_type": "application/pdf", "from": "your-name", "key": "${key}"}}'
1615
+ \`\`\``;
1616
+ })() : "";
1425
1617
  return `---
1426
1618
  name: ${name}
1427
1619
  description: ${desc}
@@ -1454,7 +1646,7 @@ curl -X POST "${sendUrl}" \\
1454
1646
  -d '{"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}'
1455
1647
  \`\`\`
1456
1648
 
1457
- Always-current copy of this skill (all ids filled in): ${skillUrl}${queueSection}`;
1649
+ Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${queueSection}`;
1458
1650
  }
1459
1651
 
1460
1652
  function resolveSender(channel, input = {}) {
@@ -2720,7 +2912,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2720
2912
  const tunnels = handlers.tunnels;
2721
2913
  if (!tunnels) throw new Error("Tunnel management not available");
2722
2914
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2723
- const { FrpcTunnel } = await import('./frpc-WpRu5zE_.mjs');
2915
+ const { FrpcTunnel } = await import('./frpc-BgabHfqV.mjs');
2724
2916
  const tunnel = new FrpcTunnel({
2725
2917
  name: params.name,
2726
2918
  ports: params.ports,
@@ -3167,7 +3359,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3167
3359
  }
3168
3360
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3169
3361
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3170
- const { toolsForRole } = await import('./sideband-B9uONcBg.mjs');
3362
+ const { toolsForRole } = await import('./sideband-BtZMx9Af.mjs');
3171
3363
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3172
3364
  return fmt(r2);
3173
3365
  }
@@ -3266,7 +3458,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3266
3458
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3267
3459
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3268
3460
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3269
- const { queryCore } = await import('./commands-B3uk2hcj.mjs');
3461
+ const { queryCore } = await import('./commands-Bxb8oEBa.mjs');
3270
3462
  const timeout = c.reply?.timeout_sec || 120;
3271
3463
  let result;
3272
3464
  try {
@@ -3371,6 +3563,44 @@ ${d?.error || "not found"}`;
3371
3563
  }
3372
3564
  await new Promise((resolve) => setTimeout(resolve, 500));
3373
3565
  }
3566
+ },
3567
+ // Safety-restricted file upload (#0093). Deny-by-default: only channels whose
3568
+ // config sets upload.enabled accept files. The upload is a pure, validated file
3569
+ // write into the channel's project folder — no agent run — so we do NOT spawn a
3570
+ // stateless one-shot; we resolve the channel + dir from disk and write directly,
3571
+ // which works whether or not a live session hosts the channel. All safety checks
3572
+ // (filename/size/extension/MIME/count/path-confinement) live in channel/upload.ts.
3573
+ upload: async (kwargs = {}, context) => {
3574
+ trackInbound();
3575
+ if (!kwargs.channel) return { error: MISSING_CHANNEL_HINT };
3576
+ const found = readChannelFromDisk(kwargs.channel);
3577
+ if (!found) return { error: "channel not found" };
3578
+ const { c, dir } = found;
3579
+ if (!uploadEnabled(c)) return { error: "uploads are not enabled on this channel" };
3580
+ const u = context?.user;
3581
+ const r = resolveSender(c, {
3582
+ key: kwargs.key,
3583
+ from: kwargs.from,
3584
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
3585
+ hyphaAnonymous: u?.is_anonymous === true,
3586
+ hyphaWorkspace: u?.scope?.current_workspace
3587
+ });
3588
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3589
+ const cfg = effectiveUploadConfig(c);
3590
+ const dec = decodeBase64Capped(kwargs.content_base64, cfg.maxBytes);
3591
+ if (dec.error || !dec.data) return { error: dec.error || "invalid content" };
3592
+ const v = validateUpload({ projectDir: dir, channel: c, filename: kwargs.filename, bytes: dec.data.length, contentType: kwargs.content_type });
3593
+ if (v.error || !v.target) return { error: v.error || "upload rejected" };
3594
+ try {
3595
+ const saved = writeUpload(dir, v.target, dec.data);
3596
+ try {
3597
+ new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + Math.random().toString(16).slice(2, 10), outcome: "uploaded" });
3598
+ } catch {
3599
+ }
3600
+ return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
3601
+ } catch (e) {
3602
+ return { ok: false, error: e?.message || String(e) };
3603
+ }
3374
3604
  }
3375
3605
  },
3376
3606
  { overwrite: true }
@@ -3536,6 +3766,32 @@ function applyInboxClear(inbox, opts) {
3536
3766
  return { kept, removed: inbox.length - kept.length };
3537
3767
  }
3538
3768
 
3769
+ function inboxFilePath(projectDir, sessionId) {
3770
+ return join(projectDir, ".svamp", sessionId, "inbox.json");
3771
+ }
3772
+ function loadInbox(projectDir, sessionId) {
3773
+ try {
3774
+ const p = inboxFilePath(projectDir, sessionId);
3775
+ if (!existsSync(p)) return [];
3776
+ const data = JSON.parse(readFileSync(p, "utf8"));
3777
+ if (Array.isArray(data)) return data;
3778
+ if (data && Array.isArray(data.messages)) return data.messages;
3779
+ return [];
3780
+ } catch {
3781
+ return [];
3782
+ }
3783
+ }
3784
+ function saveInbox(projectDir, sessionId, inbox) {
3785
+ try {
3786
+ const p = inboxFilePath(projectDir, sessionId);
3787
+ mkdirSync(dirname(p), { recursive: true });
3788
+ const tmp = `${p}.tmp-${process.pid}`;
3789
+ writeFileSync(tmp, JSON.stringify(inbox));
3790
+ renameSync(tmp, p);
3791
+ } catch {
3792
+ }
3793
+ }
3794
+
3539
3795
  function envInt(name, fallback) {
3540
3796
  const raw = process.env[name];
3541
3797
  if (raw === void 0 || raw === "") return fallback;
@@ -3760,7 +4016,9 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
3760
4016
 
3761
4017
  const PARTICIPANTS_CHANNEL_ID = "sys-participants";
3762
4018
  function channelPublicView(c) {
3763
- return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
4019
+ const u = c.upload;
4020
+ const upload = u && u.enabled === true ? { enabled: true, ...typeof u.maxBytes === "number" ? { maxBytes: u.maxBytes } : {}, ...Array.isArray(u.allowedExt) ? { allowedExt: u.allowedExt } : {} } : void 0;
4021
+ return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind, ...upload ? { upload } : {} };
3764
4022
  }
3765
4023
  function isStructuredMessage(msg) {
3766
4024
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
@@ -3933,9 +4191,16 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3933
4191
  mode: "remote",
3934
4192
  time: Date.now()
3935
4193
  };
3936
- const inbox = [];
4194
+ const inbox = loadInbox(metadata.path || process.cwd(), sessionId);
3937
4195
  const INBOX_MAX = 100;
4196
+ const persistInbox = () => {
4197
+ try {
4198
+ saveInbox(metadata.path || process.cwd(), sessionId, inbox);
4199
+ } catch {
4200
+ }
4201
+ };
3938
4202
  const syncInboxToMetadata = () => {
4203
+ persistInbox();
3939
4204
  metadata.inbox = inbox.map((m) => ({
3940
4205
  messageId: m.messageId,
3941
4206
  body: m.body,
@@ -4554,6 +4819,40 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4554
4819
  return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
4555
4820
  }
4556
4821
  },
4822
+ // Safety-restricted file upload (#0093). Deny-by-default: a channel accepts
4823
+ // uploads ONLY when its config sets upload.enabled. Auth = the channel's OWN
4824
+ // identity policy (resolveSender), same as channelSend. All validation is
4825
+ // server-side (channel/upload.ts): filename sanitization (no traversal),
4826
+ // size cap, extension/MIME allowlist, file-count cap, path confinement.
4827
+ channelUpload: async (params, context) => {
4828
+ const c = channelStore.get(params.channel);
4829
+ if (!c || c.enabled === false) return { error: "channel not found" };
4830
+ if (!uploadEnabled(c)) return { error: "uploads are not enabled on this channel" };
4831
+ const u = context?.user;
4832
+ const r = resolveSender(c, {
4833
+ key: params.key,
4834
+ from: params.from,
4835
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
4836
+ hyphaAnonymous: u?.is_anonymous === true,
4837
+ hyphaWorkspace: u?.scope?.current_workspace
4838
+ });
4839
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
4840
+ const cfg2 = effectiveUploadConfig(c);
4841
+ const dec = decodeBase64Capped(params.content_base64, cfg2.maxBytes);
4842
+ if (dec.error || !dec.data) return { error: dec.error || "invalid content" };
4843
+ const v = validateUpload({ projectDir: metadata.path, channel: c, filename: params.filename, bytes: dec.data.length, contentType: params.content_type });
4844
+ if (v.error || !v.target) return { error: v.error || "upload rejected" };
4845
+ const callId = "call_" + randomUUID().slice(0, 10);
4846
+ try {
4847
+ const saved = writeUpload(metadata.path, v.target, dec.data);
4848
+ channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "uploaded" });
4849
+ syncChannelsToMetadata();
4850
+ return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
4851
+ } catch (e) {
4852
+ channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
4853
+ return { ok: false, error: e?.message || String(e) };
4854
+ }
4855
+ },
4557
4856
  // Agent/owner answers a queued (async) channel message → channel outbox, addressed
4558
4857
  // to the original external caller. Routed here by `inbox reply` for channel-origin
4559
4858
  // inbox messages (which carry channelId + correlationId + from). Owner-gated since
@@ -8047,9 +8346,9 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
8047
8346
  function resolveTranscriptPath(cwd, claudeSessionId) {
8048
8347
  let real;
8049
8348
  try {
8050
- real = realpathSync(cwd);
8349
+ real = realpathSync$1(cwd);
8051
8350
  } catch {
8052
- real = resolve(cwd);
8351
+ real = resolve$1(cwd);
8053
8352
  }
8054
8353
  const projectId = real.replace(/[^a-zA-Z0-9-]/g, "-");
8055
8354
  const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join$1(os$1.homedir(), ".claude");
@@ -11631,7 +11930,7 @@ async function startDaemon(options) {
11631
11930
  saveExposedTunnels(list);
11632
11931
  }
11633
11932
  async function createExposedTunnel(spec) {
11634
- const { FrpcTunnel } = await import('./frpc-WpRu5zE_.mjs');
11933
+ const { FrpcTunnel } = await import('./frpc-BgabHfqV.mjs');
11635
11934
  const tunnel = new FrpcTunnel({
11636
11935
  name: spec.name,
11637
11936
  ports: spec.ports,
@@ -11651,7 +11950,7 @@ async function startDaemon(options) {
11651
11950
  return tunnel;
11652
11951
  }
11653
11952
  const tunnelRecreateState = /* @__PURE__ */ new Map();
11654
- const { ServeManager } = await import('./serveManager-C2ykbnxd.mjs');
11953
+ const { ServeManager } = await import('./serveManager-CCslS9Yt.mjs');
11655
11954
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
11656
11955
  ensureAutoInstalledSkills(logger).catch(() => {
11657
11956
  });
@@ -13516,11 +13815,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13516
13815
  });
13517
13816
  },
13518
13817
  onIssue: async (params) => {
13519
- const { issueRpc } = await import('./rpc-DgV6_G_Q.mjs');
13818
+ const { issueRpc } = await import('./rpc-C35Y8Vto.mjs');
13520
13819
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner });
13521
13820
  },
13522
13821
  onWorkflow: async (params) => {
13523
- const { workflowRpc } = await import('./rpc-CvpwmghB.mjs');
13822
+ const { workflowRpc } = await import('./rpc-DlM7ml1e.mjs');
13524
13823
  return workflowRpc(params?.cwd || directory, params || {});
13525
13824
  },
13526
13825
  onRipgrep: async (args, cwd) => {
@@ -13537,23 +13836,23 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13537
13836
  });
13538
13837
  },
13539
13838
  onReadFile: async (path) => {
13540
- const resolvedPath = resolve(directory, path);
13541
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13839
+ const resolvedPath = resolve$1(directory, path);
13840
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13542
13841
  throw new Error("Path outside working directory");
13543
13842
  }
13544
13843
  return await readSessionFileBase64(resolvedPath);
13545
13844
  },
13546
13845
  onWriteFile: async (path, content) => {
13547
- const resolvedPath = resolve(directory, path);
13548
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13846
+ const resolvedPath = resolve$1(directory, path);
13847
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13549
13848
  throw new Error("Path outside working directory");
13550
13849
  }
13551
13850
  await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
13552
13851
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
13553
13852
  },
13554
13853
  onListDirectory: async (path) => {
13555
- const resolvedDir = resolve(directory, path || ".");
13556
- if (sessionMetadata.securityContext && resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
13854
+ const resolvedDir = resolve$1(directory, path || ".");
13855
+ if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
13557
13856
  throw new Error("Path outside working directory");
13558
13857
  }
13559
13858
  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
@@ -13591,8 +13890,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13591
13890
  return null;
13592
13891
  }
13593
13892
  }
13594
- const resolvedPath = resolve(directory, treePath);
13595
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13893
+ const resolvedPath = resolve$1(directory, treePath);
13894
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13596
13895
  throw new Error("Path outside working directory");
13597
13896
  }
13598
13897
  const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
@@ -14030,11 +14329,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14030
14329
  });
14031
14330
  },
14032
14331
  onIssue: async (params) => {
14033
- const { issueRpc } = await import('./rpc-DgV6_G_Q.mjs');
14332
+ const { issueRpc } = await import('./rpc-C35Y8Vto.mjs');
14034
14333
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner });
14035
14334
  },
14036
14335
  onWorkflow: async (params) => {
14037
- const { workflowRpc } = await import('./rpc-CvpwmghB.mjs');
14336
+ const { workflowRpc } = await import('./rpc-DlM7ml1e.mjs');
14038
14337
  return workflowRpc(params?.cwd || directory, params || {});
14039
14338
  },
14040
14339
  onRipgrep: async (args, cwd) => {
@@ -14051,23 +14350,23 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14051
14350
  });
14052
14351
  },
14053
14352
  onReadFile: async (path) => {
14054
- const resolvedPath = resolve(directory, path);
14055
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14353
+ const resolvedPath = resolve$1(directory, path);
14354
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14056
14355
  throw new Error("Path outside working directory");
14057
14356
  }
14058
14357
  return await readSessionFileBase64(resolvedPath);
14059
14358
  },
14060
14359
  onWriteFile: async (path, content) => {
14061
- const resolvedPath = resolve(directory, path);
14062
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14360
+ const resolvedPath = resolve$1(directory, path);
14361
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14063
14362
  throw new Error("Path outside working directory");
14064
14363
  }
14065
14364
  await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
14066
14365
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
14067
14366
  },
14068
14367
  onListDirectory: async (path) => {
14069
- const resolvedDir = resolve(directory, path || ".");
14070
- if (sessionMetadata.securityContext && resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
14368
+ const resolvedDir = resolve$1(directory, path || ".");
14369
+ if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
14071
14370
  throw new Error("Path outside working directory");
14072
14371
  }
14073
14372
  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
@@ -14105,8 +14404,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14105
14404
  return null;
14106
14405
  }
14107
14406
  }
14108
- const resolvedPath = resolve(directory, treePath);
14109
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14407
+ const resolvedPath = resolve$1(directory, treePath);
14408
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14110
14409
  throw new Error("Path outside working directory");
14111
14410
  }
14112
14411
  const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
@@ -14524,7 +14823,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14524
14823
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
14525
14824
  if (channelHttpPort > 0) {
14526
14825
  try {
14527
- const { createChannelHttpServer } = await import('./httpServer-Cf54pQ77.mjs');
14826
+ const { createChannelHttpServer } = await import('./httpServer-B1KVQJfm.mjs');
14528
14827
  const channelHttpServer = createChannelHttpServer({
14529
14828
  getSessionIds: () => {
14530
14829
  const ids = [];
@@ -14789,7 +15088,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14789
15088
  const PING_TIMEOUT_MS = 15e3;
14790
15089
  const POST_RECONNECT_GRACE_MS = 2e4;
14791
15090
  const RECONNECT_JITTER_MS = 2500;
14792
- const { WorkflowScheduler } = await import('./scheduler-D8Xj2_vB.mjs');
15091
+ const { WorkflowScheduler } = await import('./scheduler-B3hBIkt8.mjs');
14793
15092
  const workflowScheduler = new WorkflowScheduler({
14794
15093
  projectRoots: () => {
14795
15094
  const dirs = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { a1 as applyClaudeProxyEnv, a2 as composeSessionId, a3 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a4 as generateHookSettings } from './run-DiCtjIs5.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { a1 as applyClaudeProxyEnv, a2 as composeSessionId, a3 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a4 as generateHookSettings } from './run-B3fosGEs.mjs';
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { m as resolveProjectRoot, w as cronMatches } from './run-DiCtjIs5.mjs';
2
+ import { m as resolveProjectRoot, w as cronMatches } from './run-B3fosGEs.mjs';
3
3
  import { l as listWorkflows, i as isWorkflowEnabled, c as workflowCrons, w as workflowSteps } from './store-DEZ8e-uE.mjs';
4
4
  import 'os';
5
5
  import 'fs/promises';
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-B3uk2hcj.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-Bxb8oEBa.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
@@ -93,7 +93,7 @@ async function serveAdd(args, machineId) {
93
93
  }
94
94
  }
95
95
  async function serveApply(args, machineId) {
96
- const { connectAndGetMachine } = await import('./commands-B3uk2hcj.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-Bxb8oEBa.mjs');
97
97
  const fs = await import('fs');
98
98
  const yaml = await import('yaml');
99
99
  const file = positionalArgs(args)[0];
@@ -182,7 +182,7 @@ async function serveApply(args, machineId) {
182
182
  }
183
183
  }
184
184
  async function serveRemove(args, machineId) {
185
- const { connectAndGetMachine } = await import('./commands-B3uk2hcj.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-Bxb8oEBa.mjs');
186
186
  const pos = positionalArgs(args);
187
187
  const name = pos[0];
188
188
  if (!name) {
@@ -202,7 +202,7 @@ async function serveRemove(args, machineId) {
202
202
  }
203
203
  }
204
204
  async function serveList(args, machineId) {
205
- const { connectAndGetMachine } = await import('./commands-B3uk2hcj.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-Bxb8oEBa.mjs');
206
206
  const all = hasFlag(args, "--all", "-a");
207
207
  const json = hasFlag(args, "--json");
208
208
  const sessionId = getFlag(args, "--session");
@@ -235,7 +235,7 @@ async function serveList(args, machineId) {
235
235
  }
236
236
  }
237
237
  async function serveInfo(machineId) {
238
- const { connectAndGetMachine } = await import('./commands-B3uk2hcj.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-Bxb8oEBa.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as http from 'http';
5
5
  import * as net from 'net';
6
6
  import * as path from 'path';
7
- import { k as getHyphaServerUrl, S as ServeAuth, l as hasCookieToken } from './run-DiCtjIs5.mjs';
7
+ import { k as getHyphaServerUrl, S as ServeAuth, l as hasCookieToken } from './run-B3fosGEs.mjs';
8
8
  import 'os';
9
9
  import 'fs/promises';
10
10
  import 'url';
@@ -733,7 +733,7 @@ class ServeManager {
733
733
  const mount = this.mounts.get(mountName);
734
734
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
735
735
  try {
736
- const { FrpcTunnel } = await import('./frpc-WpRu5zE_.mjs');
736
+ const { FrpcTunnel } = await import('./frpc-BgabHfqV.mjs');
737
737
  let tunnel;
738
738
  tunnel = new FrpcTunnel({
739
739
  name: tunnelName,