svamp-cli 0.2.346 → 0.2.358

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 (33) hide show
  1. package/bin/skills/artifact/SKILL.md +19 -5
  2. package/bin/skills/crew/SKILL.md +42 -11
  3. package/dist/{acpBackend-BZ1MJhQV.mjs → acpBackend-UaBD4zL3.mjs} +3 -0
  4. package/dist/{agentCommands-B2Nl7Fof.mjs → agentCommands-C_KvvYFp.mjs} +5 -5
  5. package/dist/{claudeAuth-iAerdaMr.mjs → claudeAuth-DMDvPNhO.mjs} +55 -6
  6. package/dist/cli.mjs +164 -86
  7. package/dist/{commands-D69mnkYu.mjs → commands-B19lVZjm.mjs} +10 -3
  8. package/dist/{commands-BB1Kcyt-.mjs → commands-Bmoy6Rlw.mjs} +34 -4
  9. package/dist/{commands-CiRCwjkJ.mjs → commands-CFBKkfX8.mjs} +9 -9
  10. package/dist/{commands-D1pZq3d4.mjs → commands-CcdoQIkb.mjs} +126 -25
  11. package/dist/{commands-BOpoVBOM.mjs → commands-DH_YQrAP.mjs} +5 -21
  12. package/dist/{commands-H4PsTGn8.mjs → commands-DRxANaS5.mjs} +65 -19
  13. package/dist/{commands-C2aODeVc.mjs → commands-nkvTnUJj.mjs} +29 -13
  14. package/dist/{detectIsolation-CzoDCEnh.mjs → detectIsolation-CrV8IFDv.mjs} +36 -16
  15. package/dist/{fleet-DM_Q7w69.mjs → fleet-Ca-UjJNL.mjs} +40 -15
  16. package/dist/friendlyName-B-OEwt0t.mjs +341 -0
  17. package/dist/{headlessCli-C8HzQvis.mjs → headlessCli-BpTXz6FD.mjs} +24 -16
  18. package/dist/{hookSettings-DYMTUgVA.mjs → hookSettings-CvvUhwCK.mjs} +294 -55
  19. package/dist/{friendlyName-1UZcyDhu.mjs → inboxGuard-CDqmIypF.mjs} +32 -347
  20. package/dist/index.mjs +10 -9
  21. package/dist/{loopVerify-B47sSjxe.mjs → loopVerify-CxAKJB5a.mjs} +1 -1
  22. package/dist/{notifyCommands-D-IKIxiq.mjs → notifyCommands-CCUE18Q-.mjs} +1 -1
  23. package/dist/package-DTTWvJbp.mjs +64 -0
  24. package/dist/{rpc-Cf0grcru.mjs → rpc-CAlAHQAy.mjs} +8 -3
  25. package/dist/{rpc-BB6RF4-F.mjs → rpc-CacreLQ8.mjs} +13 -9
  26. package/dist/{run-ChdsgaSD.mjs → run-Bc81iurl.mjs} +27 -8
  27. package/dist/{run-DkjwF-zA.mjs → run-DKOND4dO.mjs} +222 -73
  28. package/dist/{scheduler-BY30ZrB9.mjs → scheduler-IyLWBF8T.mjs} +28 -10
  29. package/dist/{serveAuth-DbVLRCRR.mjs → serveAuth-4M0vwMyj.mjs} +53 -11
  30. package/dist/{serveCommands-CUHUiYnZ.mjs → serveCommands-B-Ym_QXc.mjs} +8 -8
  31. package/dist/{store-Bn0ByYev.mjs → store-D8dok6_d.mjs} +62 -14
  32. package/package.json +3 -3
  33. package/dist/package-DtuINqf5.mjs +0 -64
@@ -1,5 +1,5 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import * as fs from 'fs';
2
- import { mkdirSync, chmodSync, writeFileSync, unlinkSync, existsSync, readFileSync, renameSync as renameSync$1 } from 'fs';
2
+ import { statSync, mkdirSync, chmodSync, writeFileSync, unlinkSync, existsSync, readFileSync, renameSync as renameSync$1 } from 'fs';
3
3
  import * as path from 'path';
4
4
  import path__default, { join } from 'path';
5
5
  import { randomUUID as randomUUID$1, randomBytes as randomBytes$1, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
@@ -17,15 +17,15 @@ import * as net from 'net';
17
17
  import { createServer } from 'net';
18
18
  import { homedir, platform, arch } from 'os';
19
19
  import { d as getFrpsSubdomainHost, e as getFrpsServerPort, f as getFrpsServerAddr, h as getHyphaServerUrl } from './instance-CeHz5C8v.mjs';
20
- import { S as ServeAuth, s as sanitizeLoginReturn, h as hasCookieToken, a as stripServeTokenParam, b as sanitizeForwardHeaders, c as stripServeAuthCookie } from './serveAuth-DbVLRCRR.mjs';
21
- import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync, cpSync, realpathSync, readdirSync, statSync, renameSync, appendFileSync, openSync, readSync, closeSync, fstatSync, writeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
20
+ import { S as ServeAuth, s as sanitizeLoginReturn, h as hasCookieToken, a as stripServeTokenParam, b as sanitizeForwardHeaders, c as stripServeAuthCookie } from './serveAuth-4M0vwMyj.mjs';
21
+ import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, renameSync, rmSync, cpSync, realpathSync, readdirSync, statSync as statSync$1, appendFileSync, openSync, readSync, closeSync, fstatSync, writeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
22
22
  import os, { homedir as homedir$1 } from 'node:os';
23
23
  import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
24
24
  import { e as envFlagEnabled, a as envFlagDisabled } from './envFlag-COOuJ3AH.mjs';
25
- import { normalizeProxyUrl, resolveHyphaProxyUrl, applyClaudeProxyEnv } from './claudeAuth-iAerdaMr.mjs';
25
+ import { normalizeProxyUrl, resolveHyphaProxyUrl, applyClaudeProxyEnv } from './claudeAuth-DMDvPNhO.mjs';
26
26
  import { CODEX_PROVIDER_KEY_ENV } from './codexProvider-BtxYc5Rj.mjs';
27
27
  import { spawn as spawn$1 } from 'node:child_process';
28
- import { b as applyInboxClear } from './friendlyName-1UZcyDhu.mjs';
28
+ import { applyInboxClear } from './inboxGuard-CDqmIypF.mjs';
29
29
  import { w as withFileLock, s as sleepSync } from './fileLock-BMPwCrB6.mjs';
30
30
  import { EventEmitter } from 'node:events';
31
31
  import { r as renderTemplate } from './cron-hHGdb5kU.mjs';
@@ -74,6 +74,19 @@ function levelToRole(level) {
74
74
  const entry = Object.entries(ROLE_HIERARCHY).find(([, v]) => v === level);
75
75
  return entry?.[0] ?? null;
76
76
  }
77
+ function isOwnerEquivalentStrict(context, sharing) {
78
+ if (!context?.user) return true;
79
+ if (isSameOwnerWorkspace(context)) return true;
80
+ if (!sharing) return false;
81
+ const userEmail = context.user.email;
82
+ if (userEmail && sharing.owner && userEmail.toLowerCase() === sharing.owner.toLowerCase()) return true;
83
+ return false;
84
+ }
85
+ function isAdminEquivalentStrict(context, sharing) {
86
+ if (!context?.user) return true;
87
+ if (isSameOwnerWorkspace(context)) return true;
88
+ return !!sharing && getEffectiveRole(context, sharing) === "admin";
89
+ }
77
90
  function authorizeRequest(context, sharing, requiredRole = "view") {
78
91
  if (!context?.user) return;
79
92
  if (isSameOwnerWorkspace(context)) return;
@@ -124,6 +137,18 @@ function workflowOpMinRole(op) {
124
137
  if (typeof op === "string" && WORKFLOW_READ_OPS.has(op)) return "view";
125
138
  return "admin";
126
139
  }
140
+ function issueVerifyRefusalReason(params, isOwner) {
141
+ if (isOwner) return null;
142
+ const p = params;
143
+ if (!p || typeof p !== "object") return null;
144
+ const op = typeof p.op === "string" ? p.op : "";
145
+ if (op !== "add" && op !== "edit" && op !== "triage") return null;
146
+ const v = p.verify;
147
+ const structuredIsCommand = !!v && typeof v === "object" && v.type === "command";
148
+ const flatCommand = typeof p.verifyCmd === "string" && p.verifyCmd.length > 0;
149
+ if (!structuredIsCommand && !flatCommand) return null;
150
+ return "Access denied: only the session owner may attach a command verify-cmd. It is executed by the daemon outside the sandbox when the issue is closed. Use --verify '<criterion>' (an agent-evaluated check) or --manual instead.";
151
+ }
127
152
 
128
153
  function toSchema(t) {
129
154
  return { name: t.name, description: t.description, parameters: t.parameters || { type: "object", properties: {} } };
@@ -279,6 +304,17 @@ const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
279
304
  const MAX_RESTART_DELAY_S = 300;
280
305
  const BACKOFF_RESET_WINDOW_MS = 6e4;
281
306
  const MAX_LOG_LINES = 300;
307
+ function checkWorkdir(workdir, stat = statSync) {
308
+ if (!workdir) return null;
309
+ try {
310
+ if (!stat(workdir).isDirectory()) return `workdir is not a directory: ${workdir}`;
311
+ return null;
312
+ } catch (err) {
313
+ if (err?.code === "ENOENT") return `workdir does not exist: ${workdir}`;
314
+ if (err?.code === "EACCES") return `workdir is not accessible: ${workdir}`;
315
+ return null;
316
+ }
317
+ }
282
318
  const CRASH_LOOP_FAILURE_THRESHOLD = 10;
283
319
  function accumulateCrashLoopFailure(prev, healthy, now) {
284
320
  if (healthy) return { consecutiveFailures: 0, failureWindowStart: void 0 };
@@ -567,8 +603,11 @@ class ProcessSupervisor {
567
603
  if (!file.endsWith(".json")) continue;
568
604
  try {
569
605
  const raw = await readFile(path__default.join(this.persistDir, file), "utf-8");
570
- const spec = JSON.parse(raw);
571
- const entry = this.makeEntry(spec);
606
+ const parsed = JSON.parse(raw);
607
+ const restored = parsed?.__state;
608
+ delete parsed.__state;
609
+ const spec = parsed;
610
+ const entry = this.makeEntry(spec, restored);
572
611
  this.entries.set(spec.id, entry);
573
612
  if (spec.keepAlive && !spec.desiredStopped) {
574
613
  await this.startEntry(
@@ -591,12 +630,49 @@ class ProcessSupervisor {
591
630
  console.log(`[SUPERVISOR] Restored ${loaded} supervised process(es)`);
592
631
  }
593
632
  }
594
- async persistSpec(spec) {
633
+ /**
634
+ * #1643: the crash-loop LEDGER travels with the spec.
635
+ *
636
+ * `ProcessState` is annotated "not persisted", and `makeEntry` zeroes restartCount /
637
+ * consecutiveFailures / crashLoopBackOff on every restore — so a permanently-broken keepAlive
638
+ * spec (dead upstream, taken port, vanished workdir) got a FRESH 10-restart budget on every
639
+ * daemon start, forever. It burns CRASH_LOOP_FAILURE_THRESHOLD restarts and ~15 min of
640
+ * exponential spawn churn, parks crash-loop-backoff, and starts over on the next boot —
641
+ * undermining the cap that exists because of the "11,508 restarts in 4 days" incident, and
642
+ * worst right after a reboot when several dead specs churn at once. `svamp process list` also
643
+ * showed RESTARTS 0 after every restart, which hid it. The #1633 docblock states this for the
644
+ * missing-workdir case; it is general.
645
+ *
646
+ * Written under a `__state` key so the file stays a superset of ProcessSpec: an older daemon
647
+ * ignores it, and `loadAll` strips it before the object becomes `entry.spec`.
648
+ */
649
+ async persistSpec(spec, state, failureWindowStart) {
595
650
  const filePath = path__default.join(this.persistDir, `${spec.id}.json`);
651
+ if (!state) {
652
+ const live = this.entries.get(spec.id);
653
+ if (live) {
654
+ state = live.state;
655
+ failureWindowStart = live.failureWindowStart;
656
+ }
657
+ }
596
658
  const tmpPath = `${filePath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
597
- await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
659
+ const payload = { ...spec };
660
+ if (state) {
661
+ payload.__state = {
662
+ restartCount: state.restartCount,
663
+ userRestartCount: state.userRestartCount,
664
+ consecutiveFailures: state.consecutiveFailures,
665
+ crashLoopBackOff: state.crashLoopBackOff,
666
+ failureWindowStart
667
+ };
668
+ }
669
+ await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
598
670
  await rename(tmpPath, filePath);
599
671
  }
672
+ /** #1643: rewrite this spec's file including its live crash-loop ledger. Best-effort. */
673
+ persistLedger(entry) {
674
+ void this.persistSpec(entry.spec, entry.state, entry.failureWindowStart).catch((err) => console.error(`[SUPERVISOR] Failed to persist crash-loop ledger for ${entry.spec.name}: ${err?.message || err}`));
675
+ }
600
676
  async deleteSpec(id) {
601
677
  try {
602
678
  await unlink(path__default.join(this.persistDir, `${id}.json`));
@@ -604,17 +680,22 @@ class ProcessSupervisor {
604
680
  }
605
681
  }
606
682
  // ── Internal helpers ──────────────────────────────────────────────────────
607
- makeEntry(spec) {
683
+ makeEntry(spec, restored) {
608
684
  return {
609
685
  spec,
610
686
  state: {
611
687
  id: spec.id,
612
688
  status: "pending",
613
- restartCount: 0,
614
- userRestartCount: 0,
689
+ // #1643: a restored ledger carries the crash-loop budget ACROSS the restart. Without
690
+ // it, `maxRestarts` and CRASH_LOOP_FAILURE_THRESHOLD were per-boot rather than
691
+ // lifetime caps, which is not what either of them claims to be.
692
+ restartCount: restored?.restartCount ?? 0,
693
+ userRestartCount: restored?.userRestartCount ?? 0,
615
694
  consecutiveProbeFailures: 0,
616
- consecutiveFailures: 0
695
+ consecutiveFailures: restored?.consecutiveFailures ?? 0,
696
+ crashLoopBackOff: restored?.crashLoopBackOff
617
697
  },
698
+ failureWindowStart: restored?.failureWindowStart,
618
699
  logBuffer: [],
619
700
  stopping: false
620
701
  };
@@ -654,6 +735,16 @@ class ProcessSupervisor {
654
735
  const { spec, state } = entry;
655
736
  if (entry.deleted) return;
656
737
  state.consecutiveProbeFailures = 0;
738
+ const workdirProblem = checkWorkdir(spec.workdir);
739
+ if (workdirProblem) {
740
+ state.status = "failed";
741
+ state.stoppedAt = Date.now();
742
+ entry.logBuffer.push(`[supervisor] ${workdirProblem}`);
743
+ console.error(
744
+ `[SUPERVISOR] Not starting '${spec.name}': ${workdirProblem} (permanent \u2014 not retrying; fix the path or run 'svamp process restart ${spec.name}')`
745
+ );
746
+ return;
747
+ }
657
748
  try {
658
749
  const env = { ...process.env, ...spec.env ?? {} };
659
750
  const child = spawn(spec.command, spec.args, {
@@ -735,6 +826,7 @@ class ProcessSupervisor {
735
826
  );
736
827
  return;
737
828
  }
829
+ this.persistLedger(entry);
738
830
  if (spec.maxRestarts > 0 && state.restartCount >= spec.maxRestarts) {
739
831
  console.warn(`[SUPERVISOR] Process '${spec.name}' reached max restarts (${spec.maxRestarts}), not restarting`);
740
832
  state.status = "failed";
@@ -758,6 +850,7 @@ class ProcessSupervisor {
758
850
  if (entry.child) return;
759
851
  state.restartCount++;
760
852
  state.status = "starting";
853
+ this.persistLedger(entry);
761
854
  this.spawnProcess(entry);
762
855
  }, delayMs);
763
856
  }
@@ -2068,9 +2161,10 @@ function serveStaticMount(req, res, opts) {
2068
2161
  return;
2069
2162
  }
2070
2163
  if (st.isDirectory()) {
2071
- const reqPath = (req.url || "/").split("?")[0];
2072
- if (!reqPath.endsWith("/")) {
2073
- res.writeHead(302, { ...CORS, Location: reqPath + "/" });
2164
+ const normalizedPath = (relPath || "/").split("?")[0];
2165
+ const redirectTo = ("/" + normalizedPath.replace(/^\/+/, "")).replace(/\/+$/, "") + "/";
2166
+ if (!normalizedPath.endsWith("/")) {
2167
+ res.writeHead(302, { ...CORS, Location: mountUrlPrefix ? mountUrlPrefix.replace(/\/$/, "") + redirectTo : redirectTo });
2074
2168
  res.end();
2075
2169
  return;
2076
2170
  }
@@ -2251,7 +2345,13 @@ function resolveMountOwnerEmail(suppliedOwnerEmail, callerEmail, machineOwnerEma
2251
2345
  function sanitizeMountForRole(mount, isAdmin) {
2252
2346
  if (isAdmin) return mount;
2253
2347
  const { linkToken: _lt, directory: _dir, process: proc, url, ...rest } = mount;
2254
- const safeProcess = proc ? { ...proc, env: proc.env ? Object.fromEntries(Object.keys(proc.env).map((k) => [k, "***"])) : proc.env } : proc;
2348
+ const safeProcess = proc ? (() => {
2349
+ const { env, workdir: _wd, command: _cmd, args: _args, ...procRest } = proc;
2350
+ return {
2351
+ ...procRest,
2352
+ env: env ? Object.fromEntries(Object.keys(env).map((k) => [k, "***"])) : env
2353
+ };
2354
+ })() : proc;
2255
2355
  const safeUrl = mount.access === "link" ? void 0 : url;
2256
2356
  return { ...rest, process: safeProcess, ...safeUrl !== void 0 ? { url: safeUrl } : {} };
2257
2357
  }
@@ -3410,6 +3510,13 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3410
3510
  res.end("Write access denied: public mounts are read-only.");
3411
3511
  return;
3412
3512
  }
3513
+ const writeBadSeg = hiddenSegmentOf(basePath);
3514
+ if (writeBadSeg) {
3515
+ this.log(`Write DENY '${mountName}': blocked dotfile segment '${writeBadSeg}' (${basePath})`);
3516
+ res.writeHead(403, { "Content-Type": "text/plain" });
3517
+ res.end("Forbidden: hidden/dot-prefixed paths are not writable.");
3518
+ return;
3519
+ }
3413
3520
  const filePath = resolveContained();
3414
3521
  if (!filePath) {
3415
3522
  this.log(`Write DENY '${mountName}': path escapes mount root (${basePath})`);
@@ -3419,13 +3526,17 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3419
3526
  }
3420
3527
  if (req.method === "PUT") {
3421
3528
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
3422
- const ws = fs.createWriteStream(filePath);
3529
+ const tmpPath = path.join(
3530
+ path.dirname(filePath),
3531
+ `.svamp-put-${process.pid}-${crypto.randomBytes(6).toString("hex")}.part`
3532
+ );
3533
+ const ws = fs.createWriteStream(tmpPath);
3423
3534
  let done = false;
3424
3535
  const cleanupPartial = () => {
3425
3536
  if (done) return;
3426
3537
  done = true;
3427
3538
  ws.destroy();
3428
- fs.unlink(filePath, () => {
3539
+ fs.unlink(tmpPath, () => {
3429
3540
  });
3430
3541
  };
3431
3542
  req.on("aborted", cleanupPartial);
@@ -3434,13 +3545,25 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
3434
3545
  });
3435
3546
  req.pipe(ws);
3436
3547
  ws.on("finish", () => {
3548
+ if (done) return;
3437
3549
  done = true;
3550
+ try {
3551
+ fs.renameSync(tmpPath, filePath);
3552
+ } catch (err) {
3553
+ fs.unlink(tmpPath, () => {
3554
+ });
3555
+ res.writeHead(500);
3556
+ res.end(err?.message || "rename failed");
3557
+ return;
3558
+ }
3438
3559
  res.writeHead(201);
3439
3560
  res.end();
3440
3561
  });
3441
3562
  ws.on("error", (err) => {
3442
3563
  if (done) return;
3443
3564
  done = true;
3565
+ fs.unlink(tmpPath, () => {
3566
+ });
3444
3567
  res.writeHead(500);
3445
3568
  res.end(err.message);
3446
3569
  });
@@ -3823,9 +3946,13 @@ function loadAccounts(svampHome) {
3823
3946
  if (!existsSync$1(file)) return [];
3824
3947
  try {
3825
3948
  const parsed = JSON.parse(readFileSync$1(file, "utf-8"));
3826
- if (!parsed || !Array.isArray(parsed.accounts)) return [];
3949
+ if (!parsed || !Array.isArray(parsed.accounts)) {
3950
+ console.warn(`[accounts] ${file} is present but malformed \u2014 treating as empty. Configured backend accounts will appear to have vanished; restore from a backup rather than re-adding blindly. (#1629)`);
3951
+ return [];
3952
+ }
3827
3953
  return parsed.accounts;
3828
- } catch {
3954
+ } catch (err) {
3955
+ console.warn(`[accounts] failed to read ${file}: ${err?.message || err}. Treating as empty \u2014 every configured backend account will appear to have vanished. (#1629)`);
3829
3956
  return [];
3830
3957
  }
3831
3958
  }
@@ -3834,7 +3961,13 @@ function saveAccounts(accounts, svampHome) {
3834
3961
  const dir = join$1(file, "..");
3835
3962
  if (!existsSync$1(dir)) mkdirSync$1(dir, { recursive: true });
3836
3963
  const payload = { version: 1, accounts };
3837
- writeFileSync$1(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
3964
+ const tmp = `${file}.tmp-${process.pid}`;
3965
+ writeFileSync$1(tmp, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
3966
+ try {
3967
+ chmodSync$1(tmp, 384);
3968
+ } catch {
3969
+ }
3970
+ renameSync(tmp, file);
3838
3971
  try {
3839
3972
  chmodSync$1(file, 384);
3840
3973
  } catch {
@@ -4520,7 +4653,7 @@ function validateUpload(input) {
4520
4653
  if (existsSync$1(destDir)) {
4521
4654
  const count = readdirSync(destDir).filter((f) => {
4522
4655
  try {
4523
- return statSync(join$1(destDir, f)).isFile();
4656
+ return statSync$1(join$1(destDir, f)).isFile();
4524
4657
  } catch {
4525
4658
  return false;
4526
4659
  }
@@ -4733,7 +4866,7 @@ class ChannelStore {
4733
4866
  list() {
4734
4867
  let st;
4735
4868
  try {
4736
- const s = statSync(this.dir);
4869
+ const s = statSync$1(this.dir);
4737
4870
  st = { mtimeMs: s.mtimeMs, size: s.size };
4738
4871
  } catch {
4739
4872
  _channelListCache.delete(this.dir);
@@ -5202,7 +5335,7 @@ class ChannelOutbox {
5202
5335
  /** Current on-disk (mtimeMs, size), or (0, -1) if the file does not exist. */
5203
5336
  _stat() {
5204
5337
  try {
5205
- const st = statSync(this.file);
5338
+ const st = statSync$1(this.file);
5206
5339
  return { mtimeMs: st.mtimeMs, size: st.size };
5207
5340
  } catch {
5208
5341
  return { mtimeMs: 0, size: -1 };
@@ -5442,6 +5575,9 @@ const SESSION_METHOD_MIN_ROLE = {
5442
5575
  channelReply: "admin",
5443
5576
  updateAgentState: "admin",
5444
5577
  switchMode: "admin",
5578
+ // #1628: rotating the outpost token DESTROYS a live capability and mints a new one — admin,
5579
+ // like outpostConnectInfo which hands the same capability out.
5580
+ outpostRotateToken: "admin",
5445
5581
  restartClaude: "admin",
5446
5582
  archiveSession: "admin",
5447
5583
  readFile: "admin",
@@ -5977,6 +6113,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
5977
6113
  const callerEmail = context?.user?.email;
5978
6114
  const machineOwner = currentMetadata.sharing?.owner;
5979
6115
  const isSharedUser = callerEmail && machineOwner && callerEmail.toLowerCase() !== machineOwner.toLowerCase();
6116
+ if (options.sharing?.allowedUsers?.length) {
6117
+ options = {
6118
+ ...options,
6119
+ sharing: {
6120
+ ...options.sharing,
6121
+ allowedUsers: options.sharing.allowedUsers.map((u) => normalizeAllowedUser(u, "spawn"))
6122
+ }
6123
+ };
6124
+ }
5980
6125
  if (isSharedUser) {
5981
6126
  const sharing = {
5982
6127
  enabled: true,
@@ -6014,7 +6159,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6014
6159
  } else if (options.sharing?.enabled && !options.sharing.owner && callerEmail) {
6015
6160
  options = {
6016
6161
  ...options,
6017
- sharing: { ...options.sharing, owner: callerEmail }
6162
+ sharing: {
6163
+ ...options.sharing,
6164
+ owner: callerEmail,
6165
+ ...options.sharing.allowedUsers ? { allowedUsers: options.sharing.allowedUsers.map((u) => normalizeAllowedUser(u, "spawn-explicit")) } : {}
6166
+ }
6018
6167
  };
6019
6168
  }
6020
6169
  if (currentMetadata.securityContextConfig && callerEmail) {
@@ -6356,12 +6505,12 @@ async function registerMachineService(server, machineId, metadata, daemonState,
6356
6505
  return { success: true };
6357
6506
  },
6358
6507
  // Shell access
6359
- bash: async (command, cwd, context) => {
6508
+ bash: async (command, cwd, timeoutMs, context) => {
6360
6509
  authorizeRequest(context, currentMetadata.sharing, "admin");
6361
6510
  const { exec } = await import('child_process');
6362
6511
  const { homedir } = await import('os');
6363
6512
  return new Promise((resolve) => {
6364
- exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
6513
+ exec(command, { cwd: cwd || homedir(), timeout: timeoutMs ?? 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
6365
6514
  if (err) resolve(execFailureOutcome(err, stdout, stderr));
6366
6515
  else resolve(execSuccessOutcome(stdout, stderr));
6367
6516
  });
@@ -7477,7 +7626,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
7477
7626
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
7478
7627
  }
7479
7628
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
7480
- const { queryCore } = await import('./commands-D1pZq3d4.mjs');
7629
+ const { queryCore } = await import('./commands-CcdoQIkb.mjs').then(function (n) { return n.d; });
7481
7630
  const timeout = c.reply?.timeout_sec || 120;
7482
7631
  let result;
7483
7632
  let thrownSessionId;
@@ -7746,7 +7895,7 @@ ${d?.error || "not found"}`;
7746
7895
  const CHUNK_BYTES$1 = 1024 * 1024;
7747
7896
  const LF = 10;
7748
7897
  function countFileLines(path) {
7749
- const size = statSync(path).size;
7898
+ const size = statSync$1(path).size;
7750
7899
  if (size === 0) return 0;
7751
7900
  const fd = openSync(path, "r");
7752
7901
  try {
@@ -7864,6 +8013,14 @@ function loadInbox(projectDir, sessionId) {
7864
8013
  return [];
7865
8014
  }
7866
8015
  }
8016
+ const reportedInboxWriteErrnos = /* @__PURE__ */ new Set();
8017
+ function reportInboxWriteFailure(projectDir, sessionId, err) {
8018
+ const code = String(err?.code || err?.name || "UNKNOWN");
8019
+ if (reportedInboxWriteErrnos.has(code)) return;
8020
+ reportedInboxWriteErrnos.add(code);
8021
+ const hint = code === "ENOSPC" ? ' \u2014 THE DISK IS FULL. Pending inter-agent messages will be LOST on the next daemon restart and `inbox reply` will answer "not found"; session transcripts are likely failing too.' : code === "EROFS" ? " \u2014 the filesystem is read-only." : code === "EACCES" || code === "EPERM" ? " \u2014 permission denied on the session .svamp directory." : code === "ENOTDIR" ? " \u2014 a path component of .svamp/<sessionId>/ is a FILE, not a directory." : "";
8022
+ console.error(`[inbox] persist failed for session ${sessionId} in ${projectDir} (${code})${hint} Further ${code} failures will not be repeated. (#1714)`);
8023
+ }
7867
8024
  function saveInbox(projectDir, sessionId, inbox) {
7868
8025
  try {
7869
8026
  const p = inboxFilePath(projectDir, sessionId);
@@ -7871,7 +8028,8 @@ function saveInbox(projectDir, sessionId, inbox) {
7871
8028
  const tmp = `${p}.tmp-${process.pid}-${randomBytes$1(4).toString("hex")}`;
7872
8029
  writeFileSync$1(tmp, JSON.stringify(inbox));
7873
8030
  renameSync(tmp, p);
7874
- } catch {
8031
+ } catch (err) {
8032
+ reportInboxWriteFailure(projectDir, sessionId, err);
7875
8033
  }
7876
8034
  }
7877
8035
  function isPending(m) {
@@ -7984,6 +8142,17 @@ function ensureOutpostToken(projectDir, session) {
7984
8142
  }
7985
8143
  return rec;
7986
8144
  }
8145
+ function rotateOutpostToken(projectDir, session) {
8146
+ const rec = { version: 1, session, token: genToken(), createdAt: Date.now() };
8147
+ const file = tokenPath(projectDir, session);
8148
+ mkdirSync$1(dirname(file), { recursive: true });
8149
+ writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
8150
+ try {
8151
+ chmodSync$1(file, 384);
8152
+ } catch {
8153
+ }
8154
+ return rec;
8155
+ }
7987
8156
  function outpostTokenMatches(rec, presented) {
7988
8157
  if (!rec || typeof presented !== "string" || presented.length !== rec.token.length) return false;
7989
8158
  let diff = 0;
@@ -8151,25 +8320,34 @@ async function _mintPairingCodeLocked(opts, resolver, ttlMs) {
8151
8320
  stage: true,
8152
8321
  _rkwargs: true
8153
8322
  });
8154
- const putUrl = await am.put_file({ artifact_id: artifactId, file_path: "pair.json", download_weight: 0, _rkwargs: true });
8155
- if (!putUrl || typeof putUrl !== "string") throw new Error(`put_file returned invalid URL: ${putUrl}`);
8156
- const controller = new AbortController();
8157
- const timer = setTimeout(() => controller.abort(), 3e4);
8158
- try {
8159
- const resp = await fetch(putUrl, {
8160
- method: "PUT",
8161
- body: pairJson,
8162
- headers: { "Content-Type": "application/json" },
8163
- signal: controller.signal
8164
- });
8165
- if (!resp.ok) throw new Error(`pair.json upload failed: ${resp.status} ${resp.statusText}`);
8166
- } finally {
8167
- clearTimeout(timer);
8168
- }
8169
- await am.commit({ artifact_id: artifactId, _rkwargs: true });
8170
8323
  const all = loadPairings(opts.svampHome);
8171
8324
  all.push({ session, code, artifactId, expiresAt, createdAt: now });
8172
8325
  savePairings(all, opts.svampHome);
8326
+ try {
8327
+ const putUrl = await am.put_file({ artifact_id: artifactId, file_path: "pair.json", download_weight: 0, _rkwargs: true });
8328
+ if (!putUrl || typeof putUrl !== "string") throw new Error(`put_file returned invalid URL: ${putUrl}`);
8329
+ const controller = new AbortController();
8330
+ const timer = setTimeout(() => controller.abort(), 3e4);
8331
+ try {
8332
+ const resp = await fetch(putUrl, {
8333
+ method: "PUT",
8334
+ body: pairJson,
8335
+ headers: { "Content-Type": "application/json" },
8336
+ signal: controller.signal
8337
+ });
8338
+ if (!resp.ok) throw new Error(`pair.json upload failed: ${resp.status} ${resp.statusText}`);
8339
+ } finally {
8340
+ clearTimeout(timer);
8341
+ }
8342
+ await am.commit({ artifact_id: artifactId, _rkwargs: true });
8343
+ } catch (err) {
8344
+ try {
8345
+ await deletePairingArtifact(am, artifactId, log);
8346
+ savePairings(loadPairings(opts.svampHome).filter((p) => p.artifactId !== artifactId), opts.svampHome);
8347
+ } catch {
8348
+ }
8349
+ throw err;
8350
+ }
8173
8351
  log?.(`[OUTPOST PAIRING] minted ${code} for ${session} (expires in ${Math.round(ttlMs / 6e4)}m)`);
8174
8352
  return { code, resolver, expiresAt };
8175
8353
  }
@@ -8395,6 +8573,24 @@ function channelPublicView(c) {
8395
8573
  function isStructuredMessage(msg) {
8396
8574
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
8397
8575
  }
8576
+ function reportInboxEviction(result, sessionId) {
8577
+ for (const m of result.droppedUndelivered) {
8578
+ console.warn(
8579
+ `[HYPHA SESSION ${sessionId}] inbox at cap: DROPPED AN UNDELIVERED message ${m.messageId || "<no id>"} from ${m.from || "<unknown>"} (urgency=${m.urgency || "normal"}). Every resident message was undelivered, so there was no delivered one to evict instead. The sender was told it was sent; it is now gone (#0328).`
8580
+ );
8581
+ }
8582
+ }
8583
+ function evictInboxOverflow(inbox, max) {
8584
+ const dropped = [];
8585
+ const droppedUndelivered = [];
8586
+ while (inbox.length > max) {
8587
+ const idx = inbox.findIndex((m) => m.handled);
8588
+ const victim = inbox.splice(idx < 0 ? 0 : idx, 1)[0];
8589
+ dropped.push(victim);
8590
+ if (!victim.handled) droppedUndelivered.push(victim);
8591
+ }
8592
+ return { dropped, droppedUndelivered };
8593
+ }
8398
8594
  function escapeXml(s) {
8399
8595
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8400
8596
  }
@@ -8430,10 +8626,10 @@ function formatInboxMessageXml(msg) {
8430
8626
  }
8431
8627
  if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
8432
8628
  if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
8433
- if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
8629
+ if (msg.urgency) attrs.push(`urgency="${escapeXml(String(msg.urgency))}"`);
8434
8630
  if (msg.replyTo) attrs.push(`reply-to="${escapeXml(msg.replyTo)}"`);
8435
8631
  if (msg.threadId) attrs.push(`thread-id="${escapeXml(msg.threadId)}"`);
8436
- attrs.push(`timestamp="${msg.timestamp}"`);
8632
+ attrs.push(`timestamp="${escapeXml(String(msg.timestamp))}"`);
8437
8633
  return `<svamp-message ${attrs.join(" ")}>
8438
8634
  ${msg.body}
8439
8635
  </svamp-message>`;
@@ -8544,7 +8740,7 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
8544
8740
  const filePath = join$1(messagesDir, "messages.jsonl");
8545
8741
  if (!existsSync$1(filePath)) return fallbackInMemory;
8546
8742
  try {
8547
- const st = statSync(filePath);
8743
+ const st = statSync$1(filePath);
8548
8744
  const cached = messageCountCache.get(filePath);
8549
8745
  if (cached && cached.size === st.size && cached.mtimeMs === st.mtimeMs) return cached.count;
8550
8746
  let count;
@@ -8803,7 +8999,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8803
8999
  } else if (role === "session") {
8804
9000
  wrappedContent = { role: "session", content: { type: "session", data: content } };
8805
9001
  } else {
8806
- const text = typeof content === "string" ? content : content?.text || content?.content || JSON.stringify(content);
9002
+ const rawText = typeof content === "string" ? content : content?.text ?? content?.content ?? content;
9003
+ const text = typeof rawText === "string" ? rawText : rawText && typeof rawText === "object" && typeof rawText.text === "string" ? rawText.text : JSON.stringify(content);
8807
9004
  wrappedContent = { role: "user", content: { type: "text", text } };
8808
9005
  }
8809
9006
  const msg = {
@@ -8853,7 +9050,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
8853
9050
  subject: `Remote outpost connected: ${who}`
8854
9051
  };
8855
9052
  inbox.push(msg);
8856
- while (inbox.length > INBOX_MAX) inbox.shift();
9053
+ reportInboxEviction(evictInboxOverflow(inbox, INBOX_MAX), sessionId);
8857
9054
  syncInboxToMetadata();
8858
9055
  callbacks.onInboxMessage?.(msg);
8859
9056
  notifyListeners({ type: "inbox-update", sessionId, message: msg });
@@ -9099,7 +9296,21 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9099
9296
  }
9100
9297
  }
9101
9298
  const isBtwEcho = !!(parsed && parsed.meta && parsed.meta.btw) || !!(meta && meta.btw);
9102
- const coerceUserContent = (c) => typeof c === "string" ? { type: "text", text: c } : c;
9299
+ const coerceUserText = (t, depth = 0) => {
9300
+ if (typeof t === "string") return t;
9301
+ if (t && typeof t === "object" && depth < 4) {
9302
+ if (typeof t.text === "string") return t.text;
9303
+ if (t.text && typeof t.text === "object") return coerceUserText(t.text, depth + 1);
9304
+ }
9305
+ return typeof t === "undefined" ? "" : JSON.stringify(t);
9306
+ };
9307
+ const coerceUserContent = (c) => {
9308
+ if (typeof c === "string") return { type: "text", text: c };
9309
+ if (c && typeof c === "object" && c.type === "text") {
9310
+ return { ...c, text: coerceUserText(c.text) };
9311
+ }
9312
+ return c;
9313
+ };
9103
9314
  const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: coerceUserContent(parsed.content) } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
9104
9315
  if (isBtwEcho) wrappedContent.meta = { btw: true };
9105
9316
  const msg = {
@@ -9276,6 +9487,32 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9276
9487
  // `svamp outpost up`, so the install command never drifts. NO channel is created — the remote
9277
9488
  // dials the machine's public `:outpost` gateway and this session's token authenticates it.
9278
9489
  // Admin-gated: the returned token is a capability to run commands on whatever machine connects.
9490
+ // #1628: REVOKE this session's outpost token by minting a fresh one, then return the new
9491
+ // install info. The durable token is a capability ("whoever holds it can run commands on
9492
+ // connected outposts") that never expired and had no rotate path — and the documented
9493
+ // workflow PRINTS it in an install one-liner meant to be pasted into someone else's
9494
+ // terminal, so it routinely lands in shell history, chat and screen shares. Every prior
9495
+ // outpost fix (#0757, #0778, #1035, #1374) closed a LEAK; none gave the operator a way to
9496
+ // invalidate a token that had already leaked.
9497
+ //
9498
+ // Admin-gated like outpostConnectInfo — this both destroys a capability and mints a new one.
9499
+ // Live pairing codes for the session are burned in the same act: leaving a code that
9500
+ // resolves to the OLD token would defeat the rotation.
9501
+ outpostRotateToken: async (params, context) => {
9502
+ authorizeRequest(context, metadata.sharing, "admin");
9503
+ if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
9504
+ try {
9505
+ rotateOutpostToken(outpostProjectDir(), sessionId);
9506
+ try {
9507
+ const am = await server.getService("public/artifact-manager");
9508
+ if (am) await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
9509
+ } catch {
9510
+ }
9511
+ } catch (err) {
9512
+ return { error: `could not rotate the outpost token: ${err?.message || err}` };
9513
+ }
9514
+ return await rpcHandlers.outpostConnectInfo(params, context);
9515
+ },
9279
9516
  outpostConnectInfo: async (params, context) => {
9280
9517
  authorizeRequest(context, metadata.sharing, "admin");
9281
9518
  if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
@@ -9694,7 +9931,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9694
9931
  authorizeRequest(context, metadata.sharing, "interact");
9695
9932
  let forwarded = params;
9696
9933
  if (params && (params.mode !== void 0 || params.allowTools !== void 0)) {
9697
- if (!roleAtLeast(getEffectiveRole(context, metadata.sharing), "admin")) {
9934
+ if (!isAdminEquivalentStrict(context, metadata.sharing)) {
9698
9935
  const { mode: _droppedMode, allowTools: _droppedTools, ...rest } = params;
9699
9936
  forwarded = rest;
9700
9937
  console.warn(
@@ -9812,6 +10049,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9812
10049
  // workflow writes 'admin' (direct server-side command exec). See issueOpMinRole above.
9813
10050
  issue: async (params, context) => {
9814
10051
  authorizeRequest(context, metadata.sharing, issueOpMinRole(params?.op));
10052
+ const refusal = issueVerifyRefusalReason(params, isOwnerEquivalentStrict(context, metadata.sharing));
10053
+ if (refusal) throw new Error(refusal);
9815
10054
  if (!callbacks.onIssue) throw new Error("issue not supported");
9816
10055
  return await callbacks.onIssue(params);
9817
10056
  },
@@ -9928,7 +10167,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
9928
10167
  const trusted = !context?.user || isSameOwnerWorkspace(context) || !!metadata.sharing && getEffectiveRole(context, metadata.sharing) === "admin";
9929
10168
  const msg = sanitizeInboundInboxMessage(message, { trusted, callerEmail: context?.user?.email });
9930
10169
  inbox.push(msg);
9931
- while (inbox.length > INBOX_MAX) inbox.shift();
10170
+ reportInboxEviction(evictInboxOverflow(inbox, INBOX_MAX), sessionId);
9932
10171
  syncInboxToMetadata();
9933
10172
  callbacks.onInboxMessage?.(msg);
9934
10173
  notifyListeners({ type: "inbox-update", sessionId, message: msg });