svamp-cli 0.2.235 → 0.2.237

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 (26) hide show
  1. package/dist/{agentCommands-CQYzhyOV.mjs → agentCommands-48COOhJK.mjs} +5 -5
  2. package/dist/{auth-DJJDXcNo.mjs → auth-CFykcr-b.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-pUbnaaJb.mjs → commands-B_N_O3m8.mjs} +6 -6
  5. package/dist/{commands-DjVnwCLk.mjs → commands-C0TWs43Y.mjs} +2 -2
  6. package/dist/{commands-BrkFOjiN.mjs → commands-C2m5ACt3.mjs} +1 -1
  7. package/dist/{commands-B9uukn75.mjs → commands-C8Eq1RMw.mjs} +1 -1
  8. package/dist/{commands-BMCNLXjz.mjs → commands-Cbpf3Qxk.mjs} +1 -1
  9. package/dist/{commands-BhP_0oWb.mjs → commands-D2MszHoT.mjs} +1 -1
  10. package/dist/{commands-C8eSHhho.mjs → commands-pJ1kxpWX.mjs} +2 -2
  11. package/dist/{fleet-DYoCJTed.mjs → fleet-CQfui55H.mjs} +2 -2
  12. package/dist/{frpc-D3UoKvqr.mjs → frpc-Ba_vkNU8.mjs} +1 -1
  13. package/dist/{headlessCli-BJt2EMMB.mjs → headlessCli-CMUr-41T.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/{package-ChjZJIJr.mjs → package-DyegLVIT.mjs} +1 -1
  16. package/dist/{pinnedClaudeCode-VIupR1NK.mjs → pinnedClaudeCode-ivV7xAp3.mjs} +15 -1
  17. package/dist/{rpc-DaRTfCjS.mjs → rpc-CTXWi-2N.mjs} +2 -2
  18. package/dist/{rpc-DPWxDUve.mjs → rpc-hilaJa6_.mjs} +1 -1
  19. package/dist/{run-Dt0YoXZ-.mjs → run-C7Zt6jLc.mjs} +1 -1
  20. package/dist/{run-CVg6eagc.mjs → run-D2CpI5au.mjs} +163 -46
  21. package/dist/{runStore-DxBjBflT.mjs → runStore-BDgSvZBQ.mjs} +3 -2
  22. package/dist/{scheduler-CPX7QBP-.mjs → scheduler-C_p2K8SX.mjs} +2 -2
  23. package/dist/{serveCommands-dQo7QYhs.mjs → serveCommands-BcrXpXod.mjs} +5 -5
  24. package/dist/{serveManager-TUKxMan6.mjs → serveManager-BzWW7OuZ.mjs} +2 -2
  25. package/dist/{sideband-Bt0Y2j3D.mjs → sideband-CO3ctHV1.mjs} +1 -1
  26. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import os from 'node:os';
4
- import { c as connectToHypha } from './run-CVg6eagc.mjs';
5
- import { PINNED_CLAUDE_CODE_VERSION } from './pinnedClaudeCode-VIupR1NK.mjs';
4
+ import { c as connectToHypha } from './run-D2CpI5au.mjs';
5
+ import { PINNED_CLAUDE_CODE_VERSION } from './pinnedClaudeCode-ivV7xAp3.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -4,7 +4,7 @@ import { mkdirSync, writeFileSync, unlinkSync, existsSync, chmodSync, readFileSy
4
4
  import { join } from 'path';
5
5
  import { homedir, platform, arch } from 'os';
6
6
  import { randomUUID, createHash } from 'crypto';
7
- import { h as getFrpsSubdomainHost, i as getFrpsServerPort, j as getFrpsServerAddr } from './run-CVg6eagc.mjs';
7
+ import { h as getFrpsSubdomainHost, i as getFrpsServerPort, j as getFrpsServerAddr } from './run-D2CpI5au.mjs';
8
8
  import 'fs/promises';
9
9
  import 'url';
10
10
  import 'node:crypto';
@@ -1,5 +1,5 @@
1
- import { P as resolveModel, a2 as describeMisconfiguration, a3 as buildMachineDeps } from './run-CVg6eagc.mjs';
2
- import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-Bt0Y2j3D.mjs';
1
+ import { P as resolveModel, a2 as describeMisconfiguration, a3 as buildMachineDeps } from './run-D2CpI5au.mjs';
2
+ import { handleRealtimeEvent, initMachineVoiceSession } from './sideband-CO3ctHV1.mjs';
3
3
  import { WebSocket } from 'ws';
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import 'os';
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-CVg6eagc.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-D2CpI5au.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.235";
2
+ var version = "0.2.237";
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";
@@ -3,6 +3,20 @@ import { promisify } from 'node:util';
3
3
 
4
4
  const PINNED_CLAUDE_CODE_VERSION = "2.1.195";
5
5
  const exec = promisify(exec$1);
6
+ let _reconcile = null;
7
+ function beginClaudeVersionReconcile(log) {
8
+ if (_reconcile) return;
9
+ _reconcile = ensureClaudeCodeVersion(log).catch((e) => {
10
+ log(`[claude-version] check failed: ${e?.message || e}`);
11
+ });
12
+ }
13
+ function awaitClaudeVersionReady(timeoutMs = 6e4) {
14
+ if (!_reconcile) return Promise.resolve();
15
+ return Promise.race([
16
+ _reconcile.then(() => void 0),
17
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
18
+ ]);
19
+ }
6
20
  async function readClaudeVersion() {
7
21
  try {
8
22
  const { stdout } = await exec("claude --version", { timeout: 1e4 });
@@ -55,4 +69,4 @@ async function ensureClaudeCodeVersion(log, pinned = PINNED_CLAUDE_CODE_VERSION,
55
69
  return { kind: "installed", from: current, to: after ?? pinned };
56
70
  }
57
71
 
58
- export { PINNED_CLAUDE_CODE_VERSION, ensureClaudeCodeVersion };
72
+ export { PINNED_CLAUDE_CODE_VERSION, awaitClaudeVersionReady, beginClaudeVersionReconcile, ensureClaudeCodeVersion };
@@ -1,6 +1,6 @@
1
- import { m as resolveProjectRoot } from './run-CVg6eagc.mjs';
1
+ import { m as resolveProjectRoot } from './run-D2CpI5au.mjs';
2
2
  import { g as getWorkflow, s as setWorkflowEnabled, r as removeWorkflow, a as saveWorkflow, b as rawWorkflow, l as listWorkflows } from './store-BTs0H_y0.mjs';
3
- import { g as getRun, l as listRuns, r as runWorkflow } from './runStore-DxBjBflT.mjs';
3
+ import { g as getRun, l as listRuns, r as runWorkflow } from './runStore-BDgSvZBQ.mjs';
4
4
  import 'os';
5
5
  import 'fs/promises';
6
6
  import 'fs';
@@ -1,4 +1,4 @@
1
- import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as resumeIssue, p as pauseIssue, q as addComment, t as addIssue, v as listIssues, w as searchIssues, x as isVisibleTo } from './run-CVg6eagc.mjs';
1
+ import { m as resolveProjectRoot, u as updateIssue, n as getIssue, o as resumeIssue, p as pauseIssue, q as addComment, t as addIssue, v as listIssues, w as searchIssues, x as isVisibleTo } from './run-D2CpI5au.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 { a4 as applyClaudeProxyEnv, a5 as composeSessionId, a6 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a7 as generateHookSettings } from './run-CVg6eagc.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { a4 as applyClaudeProxyEnv, a5 as composeSessionId, a6 as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, a7 as generateHookSettings } from './run-D2CpI5au.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';
@@ -116,6 +116,12 @@ function resolveRoleLevel(sharing, userEmail) {
116
116
  }
117
117
  return level;
118
118
  }
119
+ function roleAtLeast(role, min) {
120
+ if (!role) return false;
121
+ const have = ROLE_HIERARCHY[role];
122
+ const need = ROLE_HIERARCHY[min];
123
+ return typeof have === "number" && typeof need === "number" && have >= need;
124
+ }
119
125
  function levelToRole(level) {
120
126
  const entry = Object.entries(ROLE_HIERARCHY).find(([, v]) => v === level);
121
127
  return entry?.[0] ?? null;
@@ -1471,7 +1477,7 @@ class ChannelStore {
1471
1477
  const errs = validateChannel(c);
1472
1478
  if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
1473
1479
  mkdirSync(this.dir, { recursive: true });
1474
- const tmp = this._path(c.id) + ".tmp";
1480
+ const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
1475
1481
  writeFileSync(tmp, JSON.stringify(c, null, 2));
1476
1482
  renameSync(tmp, this._path(c.id));
1477
1483
  return c;
@@ -1708,11 +1714,13 @@ function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, no
1708
1714
 
1709
1715
  const MAX_PER_CHANNEL = 200;
1710
1716
  const TTL_MS = 60 * 60 * 1e3;
1717
+ const COMPACT_EVERY = 500;
1711
1718
  class ChannelOutbox {
1712
1719
  file;
1713
1720
  byChannel = /* @__PURE__ */ new Map();
1714
1721
  seqByChannel = /* @__PURE__ */ new Map();
1715
1722
  emitter = new EventEmitter();
1723
+ appendsSinceCompact = 0;
1716
1724
  constructor(projectDir) {
1717
1725
  const dir = join(projectDir, ".svamp", "channels");
1718
1726
  this.file = join(dir, "_outbox.jsonl");
@@ -1726,9 +1734,11 @@ class ChannelOutbox {
1726
1734
  _load() {
1727
1735
  if (!existsSync(this.file)) return;
1728
1736
  const cutoff = Date.now() - TTL_MS;
1737
+ let rawLines = 0;
1729
1738
  try {
1730
1739
  for (const line of readFileSync(this.file, "utf8").split("\n")) {
1731
1740
  if (!line.trim()) continue;
1741
+ rawLines++;
1732
1742
  let rec = null;
1733
1743
  try {
1734
1744
  rec = JSON.parse(line);
@@ -1745,6 +1755,25 @@ class ChannelOutbox {
1745
1755
  for (const [ch, arr] of this.byChannel) if (arr.length > MAX_PER_CHANNEL) this.byChannel.set(ch, arr.slice(-MAX_PER_CHANNEL));
1746
1756
  } catch {
1747
1757
  }
1758
+ let kept = 0;
1759
+ for (const arr of this.byChannel.values()) kept += arr.length;
1760
+ if (rawLines > kept) this._compact();
1761
+ }
1762
+ /** Rewrite _outbox.jsonl to the current in-memory (TTL/MAX-bounded) window. Best-effort. */
1763
+ _compact() {
1764
+ try {
1765
+ const lines = [];
1766
+ for (const [channelId, arr] of this.byChannel) {
1767
+ this._evict(channelId);
1768
+ for (const reply of this.byChannel.get(channelId) || []) lines.push(JSON.stringify({ channelId, ...reply }));
1769
+ }
1770
+ const dir = join(this.file, "..");
1771
+ mkdirSync(dir, { recursive: true });
1772
+ const tmp = `${this.file}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
1773
+ writeFileSync(tmp, lines.join("\n") + (lines.length ? "\n" : ""));
1774
+ renameSync(tmp, this.file);
1775
+ } catch {
1776
+ }
1748
1777
  }
1749
1778
  _evict(channelId) {
1750
1779
  const cutoff = Date.now() - TTL_MS;
@@ -1772,6 +1801,10 @@ class ChannelOutbox {
1772
1801
  } catch {
1773
1802
  }
1774
1803
  }
1804
+ if (++this.appendsSinceCompact >= COMPACT_EVERY) {
1805
+ this.appendsSinceCompact = 0;
1806
+ this._compact();
1807
+ }
1775
1808
  this.emitter.emit(channelId, reply);
1776
1809
  return reply;
1777
1810
  }
@@ -1855,6 +1888,25 @@ function getParamNames(fn) {
1855
1888
  }
1856
1889
  const terminalSessions = /* @__PURE__ */ new Map();
1857
1890
  let ptyModule = null;
1891
+ const TERMINAL_IDLE_MS = 30 * 60 * 1e3;
1892
+ let terminalReaper = null;
1893
+ function ensureTerminalReaper() {
1894
+ if (terminalReaper) return;
1895
+ terminalReaper = setInterval(() => {
1896
+ const now = Date.now();
1897
+ for (const [id, s] of terminalSessions) {
1898
+ if (s.exited) continue;
1899
+ if (now - s.lastActivity > TERMINAL_IDLE_MS) {
1900
+ try {
1901
+ s.pty.kill();
1902
+ } catch {
1903
+ }
1904
+ terminalSessions.delete(id);
1905
+ }
1906
+ }
1907
+ }, 6e4);
1908
+ terminalReaper.unref?.();
1909
+ }
1858
1910
  async function getPtyModule() {
1859
1911
  if (!ptyModule) {
1860
1912
  try {
@@ -2559,9 +2611,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2559
2611
  cols,
2560
2612
  rows,
2561
2613
  createdAt: Date.now(),
2614
+ lastActivity: Date.now(),
2562
2615
  cwd
2563
2616
  };
2564
2617
  terminalSessions.set(sessionId, session);
2618
+ ensureTerminalReaper();
2565
2619
  let outputBatch = "";
2566
2620
  let outputTimer = null;
2567
2621
  const flushOutput = () => {
@@ -2584,6 +2638,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2584
2638
  };
2585
2639
  ptyProcess.onData((data) => {
2586
2640
  if (!data) return;
2641
+ session.lastActivity = Date.now();
2587
2642
  outputBatch += data;
2588
2643
  if (!outputTimer) outputTimer = setTimeout(flushOutput, 16);
2589
2644
  });
@@ -2614,6 +2669,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2614
2669
  if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
2615
2670
  const filtered = filterTerminalResponses(params.data);
2616
2671
  if (filtered) session.pty.write(filtered);
2672
+ session.lastActivity = Date.now();
2617
2673
  return { success: true };
2618
2674
  },
2619
2675
  /** Resize a terminal session. */
@@ -2635,6 +2691,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2635
2691
  const session = terminalSessions.get(params.sessionId);
2636
2692
  if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
2637
2693
  const output = session.outputBuffer.splice(0).join("");
2694
+ session.lastActivity = Date.now();
2638
2695
  const result = {
2639
2696
  output,
2640
2697
  exited: session.exited
@@ -2934,7 +2991,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2934
2991
  const tunnels = handlers.tunnels;
2935
2992
  if (!tunnels) throw new Error("Tunnel management not available");
2936
2993
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2937
- const { FrpcTunnel } = await import('./frpc-D3UoKvqr.mjs');
2994
+ const { FrpcTunnel } = await import('./frpc-Ba_vkNU8.mjs');
2938
2995
  const tunnel = new FrpcTunnel({
2939
2996
  name: params.name,
2940
2997
  ports: params.ports,
@@ -3114,8 +3171,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3114
3171
  // require a signed-in, non-anonymous user so the daemon isn't an open
3115
3172
  // OpenAI relay. (No context at all = internal call → allowed.)
3116
3173
  wiseCreateEphemeralToken: async (params, context) => {
3117
- if (context && (!context.user || context.user.is_anonymous)) {
3118
- return { success: false, error: "Sign in to use WISE voice." };
3174
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) {
3175
+ return { success: false, error: "Not authorized: WISE voice requires interact access to this machine." };
3119
3176
  }
3120
3177
  const resolved = resolveModel({ provider: "openai" }, process.env);
3121
3178
  const misconfig = describeMisconfiguration(resolved);
@@ -3135,7 +3192,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3135
3192
  // the realtime WISE voice loop (connected to this session's WISE agent).
3136
3193
  // sessionId set = attached to that session; omitted = global.
3137
3194
  wiseJoinMeeting: async (params, context) => {
3138
- if (context && (!context.user || context.user.is_anonymous)) return { success: false, error: "Sign in to launch a meeting agent." };
3195
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) return { success: false, error: "Not authorized: launching a meeting agent requires interact access to this machine." };
3139
3196
  const parsed = parseMeetingUrl(params?.meetingUrl || "");
3140
3197
  if (!parsed) return { success: false, error: "Unrecognized meeting link (supports Zoom, Google Meet, Teams)." };
3141
3198
  const base = (process.env.SVAMP_VEXA_API_URL || "").replace(/\/$/, "");
@@ -3216,7 +3273,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3216
3273
  }
3217
3274
  },
3218
3275
  wiseLeaveMeeting: async (params, context) => {
3219
- if (context && (!context.user || context.user.is_anonymous)) return { success: false, error: "Sign in." };
3276
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) return { success: false, error: "Not authorized: managing a meeting agent requires interact access to this machine." };
3220
3277
  const k = params?.sessionId || (params?.platform && params?.nativeId ? `global:${params.nativeId}` : "");
3221
3278
  const rec = k ? activeMeetingAgents.get(k) : void 0;
3222
3279
  const platform = params?.platform || rec?.platform;
@@ -3233,7 +3290,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3233
3290
  }
3234
3291
  },
3235
3292
  // Drives the title-bar "WISE in a meeting" indicator (poll per session).
3236
- wiseMeetingStatus: async (params) => {
3293
+ wiseMeetingStatus: async (params, context) => {
3294
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "view")) {
3295
+ return { active: false, error: "Not authorized to view meeting status on this machine." };
3296
+ }
3237
3297
  if (params?.sessionId) {
3238
3298
  const rec = activeMeetingAgents.get(params.sessionId);
3239
3299
  let transcript = [];
@@ -3250,7 +3310,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3250
3310
  // (running inside its session) calls `svamp wise announce "<update>"` when work
3251
3311
  // is done so the room hears about it; this POSTs to the bot's Vexa chat endpoint.
3252
3312
  wiseAnnounce: async (params, context) => {
3253
- if (context && (!context.user || context.user.is_anonymous)) return { success: false, error: "Sign in to announce into a meeting." };
3313
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) return { success: false, error: "Not authorized: announcing into a meeting requires interact access to this machine." };
3254
3314
  const text = (params?.text || "").trim();
3255
3315
  if (!text) return { success: false, error: "Nothing to announce: provide text." };
3256
3316
  let rec = params?.sessionId ? activeMeetingAgents.get(params.sessionId) : void 0;
@@ -3284,8 +3344,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
3284
3344
  // which has no key post-Option-A and hits CORS/"Load failed" calling OpenAI direct.
3285
3345
  wiseSummarize: async (params, context) => {
3286
3346
  trackInbound();
3287
- if (context && (!context.user || context.user.is_anonymous)) {
3288
- return { success: false, error: "Sign in to use WISE." };
3347
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) {
3348
+ return { success: false, error: "Not authorized: WISE requires interact access to this machine." };
3289
3349
  }
3290
3350
  const text = (params?.text || "").trim();
3291
3351
  if (!text) return { success: true, answer: "(nothing to summarize)" };
@@ -3336,8 +3396,8 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3336
3396
  // Option A. The browser uses the returned ephemeral secret for its WebRTC leg.
3337
3397
  wiseMintTranscriptionToken: async (params, context) => {
3338
3398
  trackInbound();
3339
- if (context && (!context.user || context.user.is_anonymous)) {
3340
- return { success: false, error: "Sign in to use dictation." };
3399
+ if (!roleAtLeast(getEffectiveRole(context, currentMetadata.sharing), "interact")) {
3400
+ return { success: false, error: "Not authorized: dictation requires interact access to this machine." };
3341
3401
  }
3342
3402
  const resolved = resolveModel({ provider: "openai" }, process.env);
3343
3403
  const misconfig = describeMisconfiguration(resolved);
@@ -3382,7 +3442,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3382
3442
  }
3383
3443
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3384
3444
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3385
- const { toolsForRole } = await import('./sideband-Bt0Y2j3D.mjs');
3445
+ const { toolsForRole } = await import('./sideband-CO3ctHV1.mjs');
3386
3446
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3387
3447
  return fmt(r2);
3388
3448
  }
@@ -3481,7 +3541,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3481
3541
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3482
3542
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3483
3543
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3484
- const { queryCore } = await import('./commands-BMCNLXjz.mjs');
3544
+ const { queryCore } = await import('./commands-Cbpf3Qxk.mjs');
3485
3545
  const timeout = c.reply?.timeout_sec || 120;
3486
3546
  let result;
3487
3547
  try {
@@ -3651,6 +3711,7 @@ ${d?.error || "not found"}`;
3651
3711
  });
3652
3712
  },
3653
3713
  getLastInboundRpcAt: () => lastInboundRpcAt,
3714
+ getCurrentMetadata: () => currentMetadata,
3654
3715
  setHandlers: (newHandlers) => {
3655
3716
  handlers = newHandlers;
3656
3717
  },
@@ -4579,10 +4640,15 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4579
4640
  }
4580
4641
  }
4581
4642
  let forwardContent = content;
4582
- if (!trusted && parsed && parsed.role === "user" && parsed.content && typeof parsed.content === "object") {
4583
- const txt = parsed.content.text;
4584
- if (typeof txt === "string" && txt.trimStart().startsWith("<svamp-message")) {
4585
- parsed = { ...parsed, content: { ...parsed.content, text: neutralizeForgedEnvelope(txt) } };
4643
+ if (!trusted && parsed && parsed.role === "user" && parsed.content != null) {
4644
+ if (typeof parsed.content === "object") {
4645
+ const txt = parsed.content.text;
4646
+ if (typeof txt === "string" && txt.trimStart().startsWith("<svamp-message")) {
4647
+ parsed = { ...parsed, content: { ...parsed.content, text: neutralizeForgedEnvelope(txt) } };
4648
+ forwardContent = JSON.stringify(parsed);
4649
+ }
4650
+ } else if (typeof parsed.content === "string" && parsed.content.trimStart().startsWith("<svamp-message")) {
4651
+ parsed = { ...parsed, content: neutralizeForgedEnvelope(parsed.content) };
4586
4652
  forwardContent = JSON.stringify(parsed);
4587
4653
  }
4588
4654
  }
@@ -4849,7 +4915,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4849
4915
  if (meetingActivity.length > 80) meetingActivity.splice(0, meetingActivity.length - 80);
4850
4916
  return { ok: true };
4851
4917
  },
4852
- getMeetingActivity: async () => ({ activity: meetingActivity.slice(-40) }),
4918
+ getMeetingActivity: async (context) => {
4919
+ authorizeRequest(context, metadata.sharing, "view");
4920
+ return { activity: meetingActivity.slice(-40) };
4921
+ },
4853
4922
  channelDescribe: async (id) => {
4854
4923
  const c = channelStore.get(id);
4855
4924
  if (!c || c.enabled === false || c.system) return { error: "not found" };
@@ -6085,19 +6154,23 @@ var projectInfo = /*#__PURE__*/Object.freeze({
6085
6154
  });
6086
6155
 
6087
6156
  async function registerDebugService(server, machineId, deps) {
6157
+ const authorize = (context, role) => authorizeRequest(context, deps.getSharing?.(), role);
6088
6158
  const serviceInfo = await server.registerService(
6089
6159
  {
6090
6160
  id: `svamp-debug-${machineId}`,
6091
6161
  name: `Svamp Debug ${machineId.slice(0, 8)}`,
6092
6162
  type: "svamp-debug",
6093
- config: { visibility: "unlisted" },
6163
+ // require_context: true Hypha injects the caller context as the last arg of every
6164
+ // method, which the authorize() gate below enforces. 'unlisted' only hides discovery.
6165
+ config: { visibility: "unlisted", require_context: true },
6094
6166
  healthCheck: async () => ({
6095
6167
  ok: true,
6096
6168
  machineId,
6097
6169
  uptime: process.uptime(),
6098
6170
  timestamp: Date.now()
6099
6171
  }),
6100
- getSessionStates: async () => {
6172
+ getSessionStates: async (context) => {
6173
+ authorize(context, "view");
6101
6174
  const sessions = deps.getTrackedSessions();
6102
6175
  const states = [];
6103
6176
  for (const s of sessions) {
@@ -6120,27 +6193,33 @@ async function registerDebugService(server, machineId, deps) {
6120
6193
  }
6121
6194
  return states;
6122
6195
  },
6123
- getMachineInfo: async () => ({
6124
- machineId,
6125
- hostname: (await import('os')).hostname(),
6126
- platform: (await import('os')).platform(),
6127
- uptime: process.uptime(),
6128
- sessions: deps.getTrackedSessions().length
6129
- }),
6196
+ getMachineInfo: async (context) => {
6197
+ authorize(context, "view");
6198
+ return {
6199
+ machineId,
6200
+ hostname: (await import('os')).hostname(),
6201
+ platform: (await import('os')).platform(),
6202
+ uptime: process.uptime(),
6203
+ sessions: deps.getTrackedSessions().length
6204
+ };
6205
+ },
6130
6206
  // ── Artifact Sync RPCs ──
6131
- getArtifactSyncStatus: async () => {
6207
+ getArtifactSyncStatus: async (context) => {
6208
+ authorize(context, "view");
6132
6209
  const sync = deps.getArtifactSync?.();
6133
6210
  if (!sync) return { error: "Artifact sync not available" };
6134
6211
  return sync.getStatus();
6135
6212
  },
6136
- forceSyncAll: async () => {
6213
+ forceSyncAll: async (context) => {
6214
+ authorize(context, "admin");
6137
6215
  const sync = deps.getArtifactSync?.();
6138
6216
  const sessionsDir = deps.getSessionsDir?.();
6139
6217
  if (!sync || !sessionsDir) return { error: "Artifact sync not available" };
6140
6218
  await sync.syncAll(sessionsDir, machineId);
6141
6219
  return { success: true, status: sync.getStatus() };
6142
6220
  },
6143
- listRemoteSessions: async () => {
6221
+ listRemoteSessions: async (context) => {
6222
+ authorize(context, "view");
6144
6223
  const sync = deps.getArtifactSync?.();
6145
6224
  if (!sync) return [];
6146
6225
  return await sync.listRemoteSessions();
@@ -10629,8 +10708,22 @@ function addIssue(projectRoot, fields) {
10629
10708
  closed: null,
10630
10709
  body: fields.body
10631
10710
  };
10632
- atomicWrite(issuePath(projectRoot, issue.id), serializeIssue(issue));
10633
- return issue;
10711
+ mkdirSync(dirname(issuePath(projectRoot, issue.id)), { recursive: true });
10712
+ let n = Number(issue.id);
10713
+ for (let attempt = 0; attempt < 1e4; attempt++) {
10714
+ issue.id = String(n).padStart(4, "0");
10715
+ try {
10716
+ writeFileSync(issuePath(projectRoot, issue.id), serializeIssue(issue), { flag: "wx" });
10717
+ return issue;
10718
+ } catch (err) {
10719
+ if (err && err.code === "EEXIST") {
10720
+ n++;
10721
+ continue;
10722
+ }
10723
+ throw err;
10724
+ }
10725
+ }
10726
+ throw new Error("addIssue: could not allocate a free issue id");
10634
10727
  }
10635
10728
  function updateIssue(projectRoot, id, patch) {
10636
10729
  const cur = getIssue(projectRoot, id);
@@ -10716,6 +10809,7 @@ function summarize(issues) {
10716
10809
  return { total: issues.length, ready: by("ready"), in_progress: by("in_progress"), archived: by("archived") };
10717
10810
  }
10718
10811
 
10812
+ const UNBOUNDED_LOOP_ITERATIONS = 1e6;
10719
10813
  function backlogOraclePending(projectRoot, sessionId) {
10720
10814
  const inScope = (i) => !sessionId || isVisibleTo(i, sessionId);
10721
10815
  return listIssues(projectRoot).filter(inScope).filter((i) => i.status === "ready" || i.status === "in_progress").map((i) => ({ id: i.id }));
@@ -10809,7 +10903,7 @@ function checkLoopBudget(ledger, limits, now, startedAt) {
10809
10903
  const L = limits || {};
10810
10904
  const led = ledger || { turns: 0, tokens: 0, window: [] };
10811
10905
  const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10812
- if (pos(L.max_iterations) && led.turns >= L.max_iterations) {
10906
+ if (pos(L.max_iterations) && L.max_iterations < UNBOUNDED_LOOP_ITERATIONS && led.turns >= L.max_iterations) {
10813
10907
  return { exceeded: true, reason: `iteration budget reached (${led.turns}/${L.max_iterations} turns)` };
10814
10908
  }
10815
10909
  if (pos(L.max_runtime_sec) && pos(startedAt)) {
@@ -11721,7 +11815,8 @@ function writeSvampConfig(configPath, config) {
11721
11815
  renameSync$1(tmpPath, configPath);
11722
11816
  return content;
11723
11817
  }
11724
- const DEFAULT_LOOP_MAX_ITERATIONS = 1e3;
11818
+ const DEFAULT_LOOP_MAX_ITERATIONS = UNBOUNDED_LOOP_ITERATIONS;
11819
+ const LOOP_CHECKPOINT_CADENCE = 25;
11725
11820
  function getLoopDir(directory, sessionId) {
11726
11821
  if (sessionId) {
11727
11822
  const scoped = join$1(directory, ".svamp", sessionId, "loop");
@@ -12100,6 +12195,19 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12100
12195
  sessionService.pushMessage({ type: "message", message: 'No loop to modify \u2014 start one with: svamp session loop <id> "<task>" --until "<criteria>" --max N', level: "warning" }, "event");
12101
12196
  } else {
12102
12197
  const newMax = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : void 0;
12198
+ const recomposedTask = (() => {
12199
+ try {
12200
+ const gt = existing.goal_task;
12201
+ const un = existing.until;
12202
+ const or = existing.oracle;
12203
+ const effMax = newMax != null ? newMax : existing.max_iterations;
12204
+ if (typeof effMax === "number" && effMax > 0 && (gt || un)) {
12205
+ return buildGoalCondition({ task: gt, until: un, oracle: or, maxIterations: Math.min(effMax, LOOP_CHECKPOINT_CADENCE) });
12206
+ }
12207
+ } catch {
12208
+ }
12209
+ return void 0;
12210
+ })();
12103
12211
  try {
12104
12212
  const lcPath = join$1(getLoopDir(directory, sessionId), "loop.config.json");
12105
12213
  const lc = readLoopJson(directory, sessionId, "loop.config.json") || {};
@@ -12120,6 +12228,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12120
12228
  phase: "continue",
12121
12229
  iteration: 0,
12122
12230
  ...newMax != null ? { max_iterations: newMax } : {},
12231
+ ...recomposedTask ? { task: recomposedTask } : {},
12123
12232
  ...bumpedBudget ? { budget: { ...existing.budget || {}, ...bumpedBudget } } : {},
12124
12233
  completed_at: void 0,
12125
12234
  gave_up_reason: void 0,
@@ -12138,6 +12247,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12138
12247
  if (cur) writeGoalLoopState(directory, sessionId, {
12139
12248
  ...cur,
12140
12249
  ...newMax != null ? { max_iterations: newMax } : {},
12250
+ ...recomposedTask ? { task: recomposedTask } : {},
12141
12251
  ...bumpedBudget ? { budget: { ...cur.budget || {}, ...bumpedBudget } } : {}
12142
12252
  });
12143
12253
  } catch {
@@ -12151,7 +12261,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12151
12261
  const explicitMax = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : void 0;
12152
12262
  const hasExplicitMax = typeof explicitMax === "number" && explicitMax > 0;
12153
12263
  const hardMax = hasExplicitMax ? explicitMax : DEFAULT_LOOP_MAX_ITERATIONS;
12154
- const softTurnCap = hasExplicitMax ? explicitMax : 20;
12264
+ const softTurnCap = Math.min(hardMax, LOOP_CHECKPOINT_CADENCE);
12155
12265
  const condition = buildGoalCondition({ task, until, oracle, maxIterations: softTurnCap });
12156
12266
  const budget = parseLoopBudget(cfg.budget);
12157
12267
  try {
@@ -12607,7 +12717,7 @@ async function startDaemon(options) {
12607
12717
  saveExposedTunnels(list);
12608
12718
  }
12609
12719
  async function createExposedTunnel(spec) {
12610
- const { FrpcTunnel } = await import('./frpc-D3UoKvqr.mjs');
12720
+ const { FrpcTunnel } = await import('./frpc-Ba_vkNU8.mjs');
12611
12721
  const tunnel = new FrpcTunnel({
12612
12722
  name: spec.name,
12613
12723
  ports: spec.ports,
@@ -12627,15 +12737,15 @@ async function startDaemon(options) {
12627
12737
  return tunnel;
12628
12738
  }
12629
12739
  const tunnelRecreateState = /* @__PURE__ */ new Map();
12630
- const { ServeManager } = await import('./serveManager-TUKxMan6.mjs');
12740
+ const { ServeManager } = await import('./serveManager-BzWW7OuZ.mjs');
12631
12741
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
12632
12742
  ensureAutoInstalledSkills(logger).catch(() => {
12633
12743
  });
12634
12744
  ensureAutoInstalledCommands(logger);
12635
12745
  (async () => {
12636
12746
  try {
12637
- const { ensureClaudeCodeVersion } = await import('./pinnedClaudeCode-VIupR1NK.mjs');
12638
- await ensureClaudeCodeVersion((msg) => logger.log(msg));
12747
+ const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-ivV7xAp3.mjs');
12748
+ beginClaudeVersionReconcile((msg) => logger.log(msg));
12639
12749
  } catch (e) {
12640
12750
  logger.log(`[claude-version] check failed: ${e?.message || e}`);
12641
12751
  }
@@ -14636,11 +14746,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14636
14746
  });
14637
14747
  },
14638
14748
  onIssue: async (params) => {
14639
- const { issueRpc } = await import('./rpc-DPWxDUve.mjs');
14749
+ const { issueRpc } = await import('./rpc-hilaJa6_.mjs');
14640
14750
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
14641
14751
  },
14642
14752
  onWorkflow: async (params) => {
14643
- const { workflowRpc } = await import('./rpc-DaRTfCjS.mjs');
14753
+ const { workflowRpc } = await import('./rpc-CTXWi-2N.mjs');
14644
14754
  return workflowRpc(params?.cwd || directory, params || {});
14645
14755
  },
14646
14756
  onRipgrep: async (args, cwd) => {
@@ -15173,11 +15283,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15173
15283
  });
15174
15284
  },
15175
15285
  onIssue: async (params) => {
15176
- const { issueRpc } = await import('./rpc-DPWxDUve.mjs');
15286
+ const { issueRpc } = await import('./rpc-hilaJa6_.mjs');
15177
15287
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
15178
15288
  },
15179
15289
  onWorkflow: async (params) => {
15180
- const { workflowRpc } = await import('./rpc-DaRTfCjS.mjs');
15290
+ const { workflowRpc } = await import('./rpc-CTXWi-2N.mjs');
15181
15291
  return workflowRpc(params?.cwd || directory, params || {});
15182
15292
  },
15183
15293
  onRipgrep: async (args, cwd) => {
@@ -15854,8 +15964,10 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
15854
15964
  return null;
15855
15965
  },
15856
15966
  getArtifactSync: () => artifactSync,
15857
- getSessionsDir: () => SVAMP_HOME
15967
+ getSessionsDir: () => SVAMP_HOME,
15858
15968
  // Legacy; debug service uses session index now
15969
+ // SECURITY #0207: authorize debug RPCs against the machine's live sharing config.
15970
+ getSharing: () => machineService.getCurrentMetadata().sharing
15859
15971
  });
15860
15972
  logger.log(`Debug service registered: svamp-debug-${machineId}`);
15861
15973
  const supervised = process.env.SVAMP_SUPERVISED === "1";
@@ -15909,6 +16021,11 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
15909
16021
  }
15910
16022
  }
15911
16023
  if (persistedSessions.length > 0) {
16024
+ try {
16025
+ const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-ivV7xAp3.mjs');
16026
+ await awaitClaudeVersionReady();
16027
+ } catch {
16028
+ }
15912
16029
  logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
15913
16030
  const restoreConcurrency = Math.max(1, parseInt(process.env.SVAMP_RESTORE_CONCURRENCY || "8", 10) || 8);
15914
16031
  const restoreOne = async (persisted) => {
@@ -16047,7 +16164,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16047
16164
  const PING_TIMEOUT_MS = 15e3;
16048
16165
  const POST_RECONNECT_GRACE_MS = 2e4;
16049
16166
  const RECONNECT_JITTER_MS = 2500;
16050
- const { WorkflowScheduler } = await import('./scheduler-CPX7QBP-.mjs');
16167
+ const { WorkflowScheduler } = await import('./scheduler-C_p2K8SX.mjs');
16051
16168
  const workflowScheduler = new WorkflowScheduler({
16052
16169
  projectRoots: () => {
16053
16170
  const dirs = /* @__PURE__ */ new Set();
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { join } from 'node:path';
4
5
  import { c as workflowSteps, d as workflowsDir } from './store-BTs0H_y0.mjs';
5
- import { v as listIssues, t as addIssue } from './run-CVg6eagc.mjs';
6
+ import { v as listIssues, t as addIssue } from './run-D2CpI5au.mjs';
6
7
 
7
8
  const MAX_RUNS_PER_WORKFLOW = 50;
8
9
  const MAX_STREAM_CHARS = 8e3;
@@ -41,7 +42,7 @@ function recordRun(projectRoot, run) {
41
42
  if (idx >= 0) existing[idx] = run;
42
43
  else existing.push(run);
43
44
  const kept = existing.slice(-MAX_RUNS_PER_WORKFLOW);
44
- const tmp = `${file}.tmp`;
45
+ const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
45
46
  writeFileSync(tmp, kept.map((r) => JSON.stringify(r)).join("\n") + "\n");
46
47
  renameSync(tmp, file);
47
48
  } catch {