svamp-cli 0.2.195 → 0.2.196

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 (25) hide show
  1. package/dist/{agentCommands-IdOiOg0f.mjs → agentCommands-Epy2LP4k.mjs} +5 -5
  2. package/dist/{auth-DmrnqTLo.mjs → auth-BVAW-c8L.mjs} +1 -1
  3. package/dist/cli.mjs +60 -60
  4. package/dist/{commands-BlTV-jYX.mjs → commands-D3KJdH3r.mjs} +1 -1
  5. package/dist/{commands-DHI31wAh.mjs → commands-D8ad_1Aa.mjs} +1 -1
  6. package/dist/{commands-DAYpMdgB.mjs → commands-DYxBgrYh.mjs} +2 -2
  7. package/dist/{commands-B3uk2hcj.mjs → commands-DsYzp1Pc.mjs} +1 -1
  8. package/dist/{commands-DjT7M2al.mjs → commands-Dx6gz9Dr.mjs} +5 -5
  9. package/dist/{commands-DZhCJXnB.mjs → commands-i3J0d8xS.mjs} +2 -2
  10. package/dist/{commands-CbBOkn_G.mjs → commands-yNPBtne1.mjs} +1 -1
  11. package/dist/{fleet-BtwsNKuT.mjs → fleet-DwAy8i7Z.mjs} +1 -1
  12. package/dist/{frpc-WpRu5zE_.mjs → frpc-DeD80HI1.mjs} +1 -1
  13. package/dist/{headlessCli-Dy049YdV.mjs → headlessCli-Cw8ZR1H7.mjs} +2 -2
  14. package/dist/{httpServer-Cf54pQ77.mjs → httpServer-B1KVQJfm.mjs} +38 -0
  15. package/dist/index.mjs +1 -1
  16. package/dist/{package-ARvCEkBY.mjs → package-CpNcSvfm.mjs} +2 -2
  17. package/dist/{rpc-CvpwmghB.mjs → rpc-Blk-caU2.mjs} +1 -1
  18. package/dist/{rpc-DgV6_G_Q.mjs → rpc-CRpXmlRy.mjs} +1 -1
  19. package/dist/{run-D79u_Ud4.mjs → run-CBWkZyBD.mjs} +1 -1
  20. package/dist/{run-DiCtjIs5.mjs → run-Cxq7C5mA.mjs} +301 -35
  21. package/dist/{scheduler-D8Xj2_vB.mjs → scheduler-ro49WXPg.mjs} +1 -1
  22. package/dist/{serveCommands-Bc9DXaQv.mjs → serveCommands-5tBepki5.mjs} +5 -5
  23. package/dist/{serveManager-C2ykbnxd.mjs → serveManager-D0edqFb9.mjs} +2 -2
  24. package/dist/{sideband-B9uONcBg.mjs → sideband-p3YKDEQ0.mjs} +1 -1
  25. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
- import { N as resolveModel, $ as describeMisconfiguration, a0 as buildMachineDeps } from './run-DiCtjIs5.mjs';
2
- import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-B9uONcBg.mjs';
1
+ import { N as resolveModel, $ as describeMisconfiguration, a0 as buildMachineDeps } from './run-Cxq7C5mA.mjs';
2
+ import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-p3YKDEQ0.mjs';
3
3
  import { WebSocket } from 'ws';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import 'os';
@@ -160,6 +160,44 @@ data: ${JSON.stringify({ error: out.error })}
160
160
  });
161
161
  return;
162
162
  }
163
+ m = u.pathname.match(/^\/channel\/([\w.-]+)\/upload$/);
164
+ if (m) {
165
+ const cid = m[1];
166
+ const rpc2 = await findOwner(deps, cid);
167
+ if (!rpc2?.channelUpload) {
168
+ json(404, { error: "channel not found" });
169
+ return;
170
+ }
171
+ let chunks2 = "";
172
+ req.on("data", (d) => {
173
+ chunks2 += d;
174
+ if (chunks2.length > 40 * 1024 * 1024) req.destroy();
175
+ });
176
+ req.on("end", async () => {
177
+ let body = {};
178
+ try {
179
+ body = chunks2 ? JSON.parse(chunks2) : {};
180
+ } catch {
181
+ json(400, { error: "invalid JSON body" });
182
+ return;
183
+ }
184
+ try {
185
+ const out = await rpc2.channelUpload({
186
+ channel: cid,
187
+ key: keyOf(),
188
+ from: body.from,
189
+ session: body.session,
190
+ filename: body.filename,
191
+ content_base64: body.content_base64 ?? body.contentBase64,
192
+ content_type: body.content_type ?? body.contentType
193
+ });
194
+ json(out?.error ? out.error === "channel not found" ? 404 : 400 : 200, out);
195
+ } catch (e) {
196
+ json(500, { error: e?.message || String(e) });
197
+ }
198
+ });
199
+ return;
200
+ }
163
201
  m = u.pathname.match(/^\/channel\/([\w.-]+)\/tool$/);
164
202
  if (m) {
165
203
  const cid = m[1];
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-DiCtjIs5.mjs';
1
+ export { c as connectToHypha, a as createSessionStore, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, s as startDaemon, b as stopDaemon } from './run-Cxq7C5mA.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.195";
2
+ var version = "0.2.196";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
@@ -19,7 +19,7 @@ var exports$1 = {
19
19
  var scripts = {
20
20
  build: "rm -rf dist bin/skills && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/loop bin/skills/loop && cp -r ../../skills/crew bin/skills/crew && tsc --noEmit && pkgroll",
21
21
  typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-loop-activation.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs",
22
+ test: "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-loop-activation.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs",
23
23
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
24
  dev: "tsx src/cli.ts",
25
25
  "dev:daemon": "tsx src/cli.ts daemon start-sync",
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { m as resolveProjectRoot } from './run-DiCtjIs5.mjs';
2
+ import { m as resolveProjectRoot } from './run-Cxq7C5mA.mjs';
3
3
  import { g as getWorkflow, w as workflowSteps, s as setWorkflowEnabled, r as removeWorkflow, a as saveWorkflow, b as rawWorkflow, l as listWorkflows } from './store-DEZ8e-uE.mjs';
4
4
  import 'os';
5
5
  import 'fs/promises';
@@ -1,4 +1,4 @@
1
- import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as addComment, p as addIssue, q as listIssues, t as searchIssues, v as isVisibleTo } from './run-DiCtjIs5.mjs';
1
+ import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as addComment, p as addIssue, q as listIssues, t as searchIssues, v as isVisibleTo } from './run-Cxq7C5mA.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -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-Cxq7C5mA.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,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-DeD80HI1.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-p3YKDEQ0.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-DsYzp1Pc.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 }
@@ -3760,7 +3990,9 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
3760
3990
 
3761
3991
  const PARTICIPANTS_CHANNEL_ID = "sys-participants";
3762
3992
  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 };
3993
+ const u = c.upload;
3994
+ const upload = u && u.enabled === true ? { enabled: true, ...typeof u.maxBytes === "number" ? { maxBytes: u.maxBytes } : {}, ...Array.isArray(u.allowedExt) ? { allowedExt: u.allowedExt } : {} } : void 0;
3995
+ 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
3996
  }
3765
3997
  function isStructuredMessage(msg) {
3766
3998
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
@@ -4554,6 +4786,40 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4554
4786
  return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
4555
4787
  }
4556
4788
  },
4789
+ // Safety-restricted file upload (#0093). Deny-by-default: a channel accepts
4790
+ // uploads ONLY when its config sets upload.enabled. Auth = the channel's OWN
4791
+ // identity policy (resolveSender), same as channelSend. All validation is
4792
+ // server-side (channel/upload.ts): filename sanitization (no traversal),
4793
+ // size cap, extension/MIME allowlist, file-count cap, path confinement.
4794
+ channelUpload: async (params, context) => {
4795
+ const c = channelStore.get(params.channel);
4796
+ if (!c || c.enabled === false) return { error: "channel not found" };
4797
+ if (!uploadEnabled(c)) return { error: "uploads are not enabled on this channel" };
4798
+ const u = context?.user;
4799
+ const r = resolveSender(c, {
4800
+ key: params.key,
4801
+ from: params.from,
4802
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
4803
+ hyphaAnonymous: u?.is_anonymous === true,
4804
+ hyphaWorkspace: u?.scope?.current_workspace
4805
+ });
4806
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
4807
+ const cfg2 = effectiveUploadConfig(c);
4808
+ const dec = decodeBase64Capped(params.content_base64, cfg2.maxBytes);
4809
+ if (dec.error || !dec.data) return { error: dec.error || "invalid content" };
4810
+ const v = validateUpload({ projectDir: metadata.path, channel: c, filename: params.filename, bytes: dec.data.length, contentType: params.content_type });
4811
+ if (v.error || !v.target) return { error: v.error || "upload rejected" };
4812
+ const callId = "call_" + randomUUID().slice(0, 10);
4813
+ try {
4814
+ const saved = writeUpload(metadata.path, v.target, dec.data);
4815
+ channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "uploaded" });
4816
+ syncChannelsToMetadata();
4817
+ return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
4818
+ } catch (e) {
4819
+ channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
4820
+ return { ok: false, error: e?.message || String(e) };
4821
+ }
4822
+ },
4557
4823
  // Agent/owner answers a queued (async) channel message → channel outbox, addressed
4558
4824
  // to the original external caller. Routed here by `inbox reply` for channel-origin
4559
4825
  // inbox messages (which carry channelId + correlationId + from). Owner-gated since
@@ -8047,9 +8313,9 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
8047
8313
  function resolveTranscriptPath(cwd, claudeSessionId) {
8048
8314
  let real;
8049
8315
  try {
8050
- real = realpathSync(cwd);
8316
+ real = realpathSync$1(cwd);
8051
8317
  } catch {
8052
- real = resolve(cwd);
8318
+ real = resolve$1(cwd);
8053
8319
  }
8054
8320
  const projectId = real.replace(/[^a-zA-Z0-9-]/g, "-");
8055
8321
  const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join$1(os$1.homedir(), ".claude");
@@ -11631,7 +11897,7 @@ async function startDaemon(options) {
11631
11897
  saveExposedTunnels(list);
11632
11898
  }
11633
11899
  async function createExposedTunnel(spec) {
11634
- const { FrpcTunnel } = await import('./frpc-WpRu5zE_.mjs');
11900
+ const { FrpcTunnel } = await import('./frpc-DeD80HI1.mjs');
11635
11901
  const tunnel = new FrpcTunnel({
11636
11902
  name: spec.name,
11637
11903
  ports: spec.ports,
@@ -11651,7 +11917,7 @@ async function startDaemon(options) {
11651
11917
  return tunnel;
11652
11918
  }
11653
11919
  const tunnelRecreateState = /* @__PURE__ */ new Map();
11654
- const { ServeManager } = await import('./serveManager-C2ykbnxd.mjs');
11920
+ const { ServeManager } = await import('./serveManager-D0edqFb9.mjs');
11655
11921
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
11656
11922
  ensureAutoInstalledSkills(logger).catch(() => {
11657
11923
  });
@@ -13516,11 +13782,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13516
13782
  });
13517
13783
  },
13518
13784
  onIssue: async (params) => {
13519
- const { issueRpc } = await import('./rpc-DgV6_G_Q.mjs');
13785
+ const { issueRpc } = await import('./rpc-CRpXmlRy.mjs');
13520
13786
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner });
13521
13787
  },
13522
13788
  onWorkflow: async (params) => {
13523
- const { workflowRpc } = await import('./rpc-CvpwmghB.mjs');
13789
+ const { workflowRpc } = await import('./rpc-Blk-caU2.mjs');
13524
13790
  return workflowRpc(params?.cwd || directory, params || {});
13525
13791
  },
13526
13792
  onRipgrep: async (args, cwd) => {
@@ -13537,23 +13803,23 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13537
13803
  });
13538
13804
  },
13539
13805
  onReadFile: async (path) => {
13540
- const resolvedPath = resolve(directory, path);
13541
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13806
+ const resolvedPath = resolve$1(directory, path);
13807
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13542
13808
  throw new Error("Path outside working directory");
13543
13809
  }
13544
13810
  return await readSessionFileBase64(resolvedPath);
13545
13811
  },
13546
13812
  onWriteFile: async (path, content) => {
13547
- const resolvedPath = resolve(directory, path);
13548
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13813
+ const resolvedPath = resolve$1(directory, path);
13814
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13549
13815
  throw new Error("Path outside working directory");
13550
13816
  }
13551
13817
  await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
13552
13818
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
13553
13819
  },
13554
13820
  onListDirectory: async (path) => {
13555
- const resolvedDir = resolve(directory, path || ".");
13556
- if (sessionMetadata.securityContext && resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
13821
+ const resolvedDir = resolve$1(directory, path || ".");
13822
+ if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
13557
13823
  throw new Error("Path outside working directory");
13558
13824
  }
13559
13825
  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
@@ -13591,8 +13857,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
13591
13857
  return null;
13592
13858
  }
13593
13859
  }
13594
- const resolvedPath = resolve(directory, treePath);
13595
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
13860
+ const resolvedPath = resolve$1(directory, treePath);
13861
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
13596
13862
  throw new Error("Path outside working directory");
13597
13863
  }
13598
13864
  const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
@@ -14030,11 +14296,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14030
14296
  });
14031
14297
  },
14032
14298
  onIssue: async (params) => {
14033
- const { issueRpc } = await import('./rpc-DgV6_G_Q.mjs');
14299
+ const { issueRpc } = await import('./rpc-CRpXmlRy.mjs');
14034
14300
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner });
14035
14301
  },
14036
14302
  onWorkflow: async (params) => {
14037
- const { workflowRpc } = await import('./rpc-CvpwmghB.mjs');
14303
+ const { workflowRpc } = await import('./rpc-Blk-caU2.mjs');
14038
14304
  return workflowRpc(params?.cwd || directory, params || {});
14039
14305
  },
14040
14306
  onRipgrep: async (args, cwd) => {
@@ -14051,23 +14317,23 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14051
14317
  });
14052
14318
  },
14053
14319
  onReadFile: async (path) => {
14054
- const resolvedPath = resolve(directory, path);
14055
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14320
+ const resolvedPath = resolve$1(directory, path);
14321
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14056
14322
  throw new Error("Path outside working directory");
14057
14323
  }
14058
14324
  return await readSessionFileBase64(resolvedPath);
14059
14325
  },
14060
14326
  onWriteFile: async (path, content) => {
14061
- const resolvedPath = resolve(directory, path);
14062
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14327
+ const resolvedPath = resolve$1(directory, path);
14328
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14063
14329
  throw new Error("Path outside working directory");
14064
14330
  }
14065
14331
  await fs.mkdir(dirname$1(resolvedPath), { recursive: true });
14066
14332
  await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
14067
14333
  },
14068
14334
  onListDirectory: async (path) => {
14069
- const resolvedDir = resolve(directory, path || ".");
14070
- if (sessionMetadata.securityContext && resolvedDir !== resolve(directory) && !resolvedDir.startsWith(resolve(directory) + "/")) {
14335
+ const resolvedDir = resolve$1(directory, path || ".");
14336
+ if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
14071
14337
  throw new Error("Path outside working directory");
14072
14338
  }
14073
14339
  const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
@@ -14105,8 +14371,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14105
14371
  return null;
14106
14372
  }
14107
14373
  }
14108
- const resolvedPath = resolve(directory, treePath);
14109
- if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
14374
+ const resolvedPath = resolve$1(directory, treePath);
14375
+ if (sessionMetadata.securityContext && resolvedPath !== resolve$1(directory) && !resolvedPath.startsWith(resolve$1(directory) + "/")) {
14110
14376
  throw new Error("Path outside working directory");
14111
14377
  }
14112
14378
  const tree = await buildTree(resolvedPath, basename$1(resolvedPath), 0);
@@ -14524,7 +14790,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14524
14790
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
14525
14791
  if (channelHttpPort > 0) {
14526
14792
  try {
14527
- const { createChannelHttpServer } = await import('./httpServer-Cf54pQ77.mjs');
14793
+ const { createChannelHttpServer } = await import('./httpServer-B1KVQJfm.mjs');
14528
14794
  const channelHttpServer = createChannelHttpServer({
14529
14795
  getSessionIds: () => {
14530
14796
  const ids = [];
@@ -14789,7 +15055,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14789
15055
  const PING_TIMEOUT_MS = 15e3;
14790
15056
  const POST_RECONNECT_GRACE_MS = 2e4;
14791
15057
  const RECONNECT_JITTER_MS = 2500;
14792
- const { WorkflowScheduler } = await import('./scheduler-D8Xj2_vB.mjs');
15058
+ const { WorkflowScheduler } = await import('./scheduler-ro49WXPg.mjs');
14793
15059
  const workflowScheduler = new WorkflowScheduler({
14794
15060
  projectRoots: () => {
14795
15061
  const dirs = /* @__PURE__ */ new Set();