svamp-cli 0.2.300 → 0.2.301

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 (29) hide show
  1. package/dist/{adminCommands-lOuwAyXW.mjs → adminCommands-DMiliv55.mjs} +1 -1
  2. package/dist/{agentCommands-C56-TSwf.mjs → agentCommands-BCkWdFFW.mjs} +5 -5
  3. package/dist/{auth-y5EinW_c.mjs → auth-Ba1_zl1h.mjs} +1 -1
  4. package/dist/{cli-BbbzgMWX.mjs → cli-Bv0Zhf3M.mjs} +69 -69
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-Db1SMhcL.mjs → commands-B2mRza8S.mjs} +2 -2
  7. package/dist/{commands-Qz7oTXdj.mjs → commands-BA3eBROn.mjs} +1 -1
  8. package/dist/{commands-Cb00JySn.mjs → commands-BAlaK67Y.mjs} +1 -1
  9. package/dist/{commands-VtqFEtFc.mjs → commands-BC3RuhRx.mjs} +2 -2
  10. package/dist/commands-BnirT9RR.mjs +161 -0
  11. package/dist/{commands-BlVHxX-n.mjs → commands-DhB2iz0_.mjs} +7 -7
  12. package/dist/{commands-Bjbq2pJg.mjs → commands-bIioWHWe.mjs} +2 -2
  13. package/dist/{commands-BLFj8bE8.mjs → commands-hHZu5qJh.mjs} +1 -1
  14. package/dist/{fleet-DKAN08u-.mjs → fleet-BFurNsSA.mjs} +1 -1
  15. package/dist/{frpc-Bx0J5hIR.mjs → frpc-Cb6jUvED.mjs} +1 -1
  16. package/dist/{headlessCli-DaqkPy9x.mjs → headlessCli-DP9gpAxv.mjs} +2 -2
  17. package/dist/index.mjs +1 -1
  18. package/dist/{notifyCommands-DaAd67p4.mjs → notifyCommands-C0i6P1yC.mjs} +1 -1
  19. package/dist/package-D8lHDQja.mjs +64 -0
  20. package/dist/{rpc-C2l4b6gk.mjs → rpc-B-bxS-Pv.mjs} +1 -1
  21. package/dist/{rpc-D1Vg26a1.mjs → rpc-CNkfUlz1.mjs} +1 -1
  22. package/dist/{run-DrG6nwGm.mjs → run-C0GhrtVf.mjs} +347 -58
  23. package/dist/{run-CVRu9pzU.mjs → run-CvT_37Ov.mjs} +1 -1
  24. package/dist/{scheduler-CGmlf3VQ.mjs → scheduler-DT1_BBdZ.mjs} +1 -1
  25. package/dist/{serveCommands-CiYytaPO.mjs → serveCommands-CUabu97u.mjs} +5 -5
  26. package/dist/{sideband-BQz9FNc9.mjs → sideband-BlZBmbji.mjs} +1 -1
  27. package/package.json +2 -2
  28. package/dist/commands-Beysszfu.mjs +0 -293
  29. package/dist/package-B5_N_B9C.mjs +0 -64
@@ -2973,7 +2973,7 @@ Connection: close\r
2973
2973
  const mount = this.mounts.get(mountName);
2974
2974
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
2975
2975
  try {
2976
- const { FrpcTunnel } = await import('./frpc-Bx0J5hIR.mjs');
2976
+ const { FrpcTunnel } = await import('./frpc-Cb6jUvED.mjs');
2977
2977
  let tunnel;
2978
2978
  tunnel = new FrpcTunnel({
2979
2979
  name: tunnelName,
@@ -4639,11 +4639,6 @@ class ChannelOutbox {
4639
4639
  }
4640
4640
  }
4641
4641
 
4642
- var outbox = /*#__PURE__*/Object.freeze({
4643
- __proto__: null,
4644
- ChannelOutbox: ChannelOutbox
4645
- });
4646
-
4647
4642
  function getParamNames(fn) {
4648
4643
  const src = fn.toString();
4649
4644
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -5872,7 +5867,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
5872
5867
  const tunnels = handlers.tunnels;
5873
5868
  if (!tunnels) throw new Error("Tunnel management not available");
5874
5869
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
5875
- const { FrpcTunnel } = await import('./frpc-Bx0J5hIR.mjs');
5870
+ const { FrpcTunnel } = await import('./frpc-Cb6jUvED.mjs');
5876
5871
  const tunnel = new FrpcTunnel({
5877
5872
  name: params.name,
5878
5873
  ports: params.ports,
@@ -6348,7 +6343,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6348
6343
  }
6349
6344
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
6350
6345
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
6351
- const { toolsForRole } = await import('./sideband-BQz9FNc9.mjs');
6346
+ const { toolsForRole } = await import('./sideband-BlZBmbji.mjs');
6352
6347
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
6353
6348
  return fmt(r2);
6354
6349
  }
@@ -6453,7 +6448,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6453
6448
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
6454
6449
  }
6455
6450
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
6456
- const { queryCore } = await import('./commands-Cb00JySn.mjs');
6451
+ const { queryCore } = await import('./commands-BAlaK67Y.mjs');
6457
6452
  const timeout = c.reply?.timeout_sec || 120;
6458
6453
  let result;
6459
6454
  try {
@@ -6605,6 +6600,44 @@ ${d?.error || "not found"}`;
6605
6600
  { overwrite: true }
6606
6601
  );
6607
6602
  console.log(`[HYPHA MACHINE] Channels service registered: ${channelsServiceInfo.id} (type svamp-channels)`);
6603
+ const OUTPOST_MISSING_SESSION = 'missing "session" \u2014 the outpost gateway routes by session id. Over HTTP wrap args: POST {"kwargs": {"session": "<id>", "token": "<token>", "machine": "<name>"}}.';
6604
+ const routeOutpost = (session) => {
6605
+ const sid = String(session || "").trim();
6606
+ if (!sid) return { error: OUTPOST_MISSING_SESSION };
6607
+ const rpc = handlers.getSessionRPCHandlers?.(sid);
6608
+ if (!rpc?.outpostHello) return { error: `session ${sid.slice(0, 12)} is not connected on this machine` };
6609
+ return { rpc };
6610
+ };
6611
+ const outpostServiceInfo = await server.registerService(
6612
+ {
6613
+ id: "outpost",
6614
+ name: `Svamp Outpost (${currentMetadata.displayName || machineId})`,
6615
+ type: "svamp-outpost",
6616
+ // Public so a remote machine (possibly anonymous) can dial in; token-authed per call,
6617
+ // so no Hypha identity/context is needed (the handlers ignore context entirely).
6618
+ config: { visibility: "public", require_context: false },
6619
+ hello: async (kwargs = {}) => {
6620
+ trackInbound();
6621
+ const r = routeOutpost(kwargs?.session);
6622
+ if ("error" in r) return { error: r.error };
6623
+ return r.rpc.outpostHello(kwargs);
6624
+ },
6625
+ poll: async (kwargs = {}) => {
6626
+ trackInbound();
6627
+ const r = routeOutpost(kwargs?.session);
6628
+ if ("error" in r) return { error: r.error };
6629
+ return r.rpc.outpostPoll(kwargs);
6630
+ },
6631
+ result: async (kwargs = {}) => {
6632
+ trackInbound();
6633
+ const r = routeOutpost(kwargs?.session);
6634
+ if ("error" in r) return { error: r.error };
6635
+ return r.rpc.outpostResult(kwargs);
6636
+ }
6637
+ },
6638
+ { overwrite: true }
6639
+ );
6640
+ console.log(`[HYPHA MACHINE] Outpost service registered: ${outpostServiceInfo.id} (type svamp-outpost)`);
6608
6641
  return {
6609
6642
  serviceInfo,
6610
6643
  notifySessionEvent: notifyListeners,
@@ -6639,6 +6672,8 @@ ${d?.error || "not found"}`;
6639
6672
  await server.unregisterService(serviceInfo.id);
6640
6673
  await server.unregisterService(channelsServiceInfo.id).catch(() => {
6641
6674
  });
6675
+ await server.unregisterService(outpostServiceInfo.id).catch(() => {
6676
+ });
6642
6677
  }
6643
6678
  };
6644
6679
  }
@@ -6930,6 +6965,43 @@ function retryDelayMs(now, backoffUntil) {
6930
6965
  return Math.max(0, backoffUntil - now);
6931
6966
  }
6932
6967
 
6968
+ function tokenPath(projectDir, session) {
6969
+ return join$1(projectDir, ".svamp", session, "outpost.json");
6970
+ }
6971
+ function genToken() {
6972
+ return randomBytes(24).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6973
+ }
6974
+ function loadOutpostToken(projectDir, session) {
6975
+ const file = tokenPath(projectDir, session);
6976
+ if (!existsSync(file)) return null;
6977
+ try {
6978
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
6979
+ if (!parsed || typeof parsed.token !== "string" || !parsed.token) return null;
6980
+ return parsed;
6981
+ } catch {
6982
+ return null;
6983
+ }
6984
+ }
6985
+ function ensureOutpostToken(projectDir, session) {
6986
+ const existing = loadOutpostToken(projectDir, session);
6987
+ if (existing) return existing;
6988
+ const rec = { version: 1, session, token: genToken(), createdAt: Date.now() };
6989
+ const file = tokenPath(projectDir, session);
6990
+ mkdirSync$1(dirname(file), { recursive: true });
6991
+ writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
6992
+ try {
6993
+ chmodSync(file, 384);
6994
+ } catch {
6995
+ }
6996
+ return rec;
6997
+ }
6998
+ function outpostTokenMatches(rec, presented) {
6999
+ if (!rec || typeof presented !== "string" || presented.length !== rec.token.length) return false;
7000
+ let diff = 0;
7001
+ for (let i = 0; i < rec.token.length; i++) diff |= rec.token.charCodeAt(i) ^ presented.charCodeAt(i);
7002
+ return diff === 0;
7003
+ }
7004
+
6933
7005
  const DEFAULT_DOWNLOAD_BASE = "https://github.com/amun-ai/hypha-cloud/releases/latest/download";
6934
7006
  function shq(v) {
6935
7007
  return `'${String(v).replace(/'/g, `'\\''`)}'`;
@@ -6940,10 +7012,10 @@ function psq(v) {
6940
7012
  function buildOutpostInstall(params) {
6941
7013
  const base = params.serverBase.replace(/\/$/, "");
6942
7014
  const dl = (params.downloadBase || DEFAULT_DOWNLOAD_BASE).replace(/\/$/, "");
6943
- const session = params.session || params.channel;
6944
- const unix = `curl -fsSL ${shq(`${dl}/install.sh`)} | SVAMP_OUTPOST_SERVER=${shq(base)} SVAMP_OUTPOST_CHANNEL=${shq(params.channel)} SVAMP_OUTPOST_TOKEN=${shq(params.token)} SVAMP_OUTPOST_SESSION=${shq(session)} SVAMP_OUTPOST_DL=${shq(dl)} sh`;
6945
- const windows = `$env:SVAMP_OUTPOST_SERVER=${psq(base)}; $env:SVAMP_OUTPOST_CHANNEL=${psq(params.channel)}; $env:SVAMP_OUTPOST_TOKEN=${psq(params.token)}; $env:SVAMP_OUTPOST_SESSION=${psq(session)}; $env:SVAMP_OUTPOST_DL=${psq(dl)}; irm ${psq(`${dl}/install.ps1`)} | iex`;
6946
- const direct = `svamp-outpost --server ${shq(base)} --channel ${shq(params.channel)} --token ${shq(params.token)} --session ${shq(session)}`;
7015
+ const session = params.session;
7016
+ const unix = `curl -fsSL ${shq(`${dl}/install.sh`)} | SVAMP_OUTPOST_SERVER=${shq(base)} SVAMP_OUTPOST_TOKEN=${shq(params.token)} SVAMP_OUTPOST_SESSION=${shq(session)} SVAMP_OUTPOST_DL=${shq(dl)} sh`;
7017
+ const windows = `$env:SVAMP_OUTPOST_SERVER=${psq(base)}; $env:SVAMP_OUTPOST_TOKEN=${psq(params.token)}; $env:SVAMP_OUTPOST_SESSION=${psq(session)}; $env:SVAMP_OUTPOST_DL=${psq(dl)}; irm ${psq(`${dl}/install.ps1`)} | iex`;
7018
+ const direct = `svamp-outpost --server ${shq(base)} --token ${shq(params.token)} --session ${shq(session)}`;
6947
7019
  return { unix, windows, direct };
6948
7020
  }
6949
7021
 
@@ -6955,37 +7027,19 @@ const OUTPOST_BINARIES = [
6955
7027
  { label: "Linux (arm64)", file: "svamp-outpost-linux-arm64" },
6956
7028
  { label: "Windows (x86-64)", file: "svamp-outpost-windows-amd64.exe" }
6957
7029
  ];
6958
- function setupOutpostChannel(params) {
7030
+ function setupOutpost(params) {
6959
7031
  const serverUrl = params.serverUrl || "https://hypha.aicell.io";
6960
7032
  const downloadBase = (params.downloadBase || DEFAULT_OUTPOST_DOWNLOAD_BASE).replace(/\/$/, "");
6961
- const serverBase = gatewayBase(params.channelsServiceId, serverUrl);
6962
- const channelId = `outpost-${params.session}`;
6963
- const store = new ChannelStore(params.projectDir);
6964
- const existing = store.get(channelId);
6965
- let token = existing?.identity?.callers?.find((c) => c.name === "outpost")?.key;
6966
- if (!token) token = genKey();
6967
- const channel = {
6968
- id: channelId,
6969
- name: `Remote outpost \u2014 ${params.session}`,
6970
- description: "FDE remote machine: a svamp-outpost binary connects here and the agent runs commands on it.",
6971
- enabled: true,
6972
- bind: { session: params.session },
6973
- identity: { mode: "per-key", callers: [{ name: "outpost", kind: "agent", key: token }] },
6974
- action: { kind: "message" },
6975
- reply: { mode: "queue" },
6976
- skill: { name: "remote-outpost", description: "Run commands on the connected remote machine." }
6977
- };
6978
- store.save(channel);
6979
- const install = buildOutpostInstall({ serverBase, channel: channelId, token, session: params.session, downloadBase });
6980
- const connectedCount = (store.get(channelId)?.last_calls || []).filter((c) => c.sender === "outpost").length;
7033
+ const serverBase = gatewayBase(params.outpostServiceId, serverUrl);
7034
+ const { token } = ensureOutpostToken(params.projectDir, params.session);
7035
+ const install = buildOutpostInstall({ serverBase, session: params.session, token, downloadBase });
6981
7036
  return {
6982
- channelId,
6983
7037
  token,
6984
7038
  serverBase,
6985
7039
  downloadBase,
6986
7040
  install,
6987
7041
  binaries: OUTPOST_BINARIES.map((b) => ({ label: b.label, url: `${downloadBase}/${b.file}` })),
6988
- connectedCount
7042
+ connectedCount: params.connectedCount || 0
6989
7043
  };
6990
7044
  }
6991
7045
 
@@ -7070,7 +7124,6 @@ async function mintPairingCode(opts) {
7070
7124
  const expiresAt = now + ttlMs;
7071
7125
  const pairJson = JSON.stringify({
7072
7126
  server: payload.server,
7073
- channel: payload.channel,
7074
7127
  token: payload.token,
7075
7128
  session: payload.session,
7076
7129
  expiresAt
@@ -7107,6 +7160,157 @@ async function mintPairingCode(opts) {
7107
7160
  return { code, resolver, expiresAt };
7108
7161
  }
7109
7162
 
7163
+ const OUTPOST_STALE_MS = 9e4;
7164
+ const OUTPOST_EXEC_TIMEOUT_MS = 10 * 6e4;
7165
+ class OutpostCoordinator {
7166
+ conns = /* @__PURE__ */ new Map();
7167
+ queue = /* @__PURE__ */ new Map();
7168
+ pending = /* @__PURE__ */ new Map();
7169
+ seq = 0;
7170
+ now;
7171
+ setTimer;
7172
+ clearTimer;
7173
+ execTimeoutMs;
7174
+ staleMs;
7175
+ constructor(opts = {}) {
7176
+ this.now = opts.now || (() => Date.now());
7177
+ this.setTimer = opts.setTimer || ((fn, ms) => setTimeout(fn, ms));
7178
+ this.clearTimer = opts.clearTimer || ((t) => clearTimeout(t));
7179
+ this.execTimeoutMs = opts.execTimeoutMs ?? OUTPOST_EXEC_TIMEOUT_MS;
7180
+ this.staleMs = opts.staleMs ?? OUTPOST_STALE_MS;
7181
+ }
7182
+ isStale(c, at) {
7183
+ return at - c.lastSeen > this.staleMs;
7184
+ }
7185
+ /** Drop dead connections; fail any in-flight execs bound to them so callers never hang. */
7186
+ prune(at = this.now()) {
7187
+ for (const [machine, c] of [...this.conns]) {
7188
+ if (this.isStale(c, at)) {
7189
+ this.conns.delete(machine);
7190
+ this.queue.delete(machine);
7191
+ this.failMachine(machine, "the remote outpost disconnected");
7192
+ }
7193
+ }
7194
+ }
7195
+ /** Reject every pending exec on a machine with a synthetic disconnect result (exitCode -1). */
7196
+ failMachine(machine, reason) {
7197
+ for (const [id, p] of [...this.pending]) {
7198
+ if (p.machine !== machine) continue;
7199
+ this.clearTimer(p.timer);
7200
+ this.pending.delete(id);
7201
+ p.resolve({ id, stdout: "", stderr: reason, exitCode: -1, synthetic: true, ts: this.now() });
7202
+ }
7203
+ }
7204
+ /**
7205
+ * A remote said hello (first connect OR reconnect after an outage). Upserts the connection and
7206
+ * returns `isNew` = true when it should trigger the ONE-TIME inbox announce — true on the first
7207
+ * appearance and on a reconnect after the connection had gone stale/pruned.
7208
+ */
7209
+ registerConnection(machine, meta = {}) {
7210
+ const at = this.now();
7211
+ this.prune(at);
7212
+ const existing = this.conns.get(machine);
7213
+ const isNew = !existing;
7214
+ const connection = {
7215
+ machine,
7216
+ label: meta.label ?? existing?.label,
7217
+ os: meta.os ?? existing?.os,
7218
+ arch: meta.arch ?? existing?.arch,
7219
+ version: meta.version ?? existing?.version,
7220
+ connectedAt: existing?.connectedAt ?? at,
7221
+ lastSeen: at
7222
+ };
7223
+ this.conns.set(machine, connection);
7224
+ return { isNew, connection };
7225
+ }
7226
+ /** Refresh liveness for an active poll (keeps the connection "always on"). */
7227
+ touch(machine) {
7228
+ const c = this.conns.get(machine);
7229
+ if (!c) return false;
7230
+ c.lastSeen = this.now();
7231
+ return true;
7232
+ }
7233
+ /** Live connections only (prunes first). */
7234
+ listConnections() {
7235
+ this.prune();
7236
+ return [...this.conns.values()].sort((a, b) => a.connectedAt - b.connectedAt);
7237
+ }
7238
+ isConnected(machine) {
7239
+ this.prune();
7240
+ return this.conns.has(machine);
7241
+ }
7242
+ /**
7243
+ * Pick the machine an exec should target: an explicit live target, or the sole live outpost.
7244
+ * Ambiguity (0 or >1 without a target) is a clear error, never a silent guess.
7245
+ */
7246
+ resolveTarget(target) {
7247
+ const live = this.listConnections();
7248
+ if (target) {
7249
+ const hit = live.find((c) => c.machine === target || c.label === target);
7250
+ if (!hit) return { error: `no connected outpost named "${target}" (connected: ${live.map((c) => c.machine).join(", ") || "none"})` };
7251
+ return { machine: hit.machine };
7252
+ }
7253
+ if (live.length === 0) return { error: "no remote outpost is connected \u2014 run the install one-liner on the remote machine first" };
7254
+ if (live.length > 1) return { error: `multiple outposts connected (${live.map((c) => c.machine).join(", ")}) \u2014 pass a target` };
7255
+ return { machine: live[0].machine };
7256
+ }
7257
+ /**
7258
+ * Enqueue a command for a machine and return a promise that resolves with the REAL result once
7259
+ * the outpost posts it back (or a synthetic exitCode -1 on disconnect/timeout). Fails fast if the
7260
+ * machine is not connected.
7261
+ */
7262
+ enqueueExec(machine, cmd, cwd) {
7263
+ const id = `x${(++this.seq).toString(36)}_${this.now().toString(36)}`;
7264
+ if (!this.isConnected(machine)) {
7265
+ return {
7266
+ id,
7267
+ promise: Promise.resolve({ id, stdout: "", stderr: `outpost "${machine}" is not connected`, exitCode: -1, synthetic: true, ts: this.now() })
7268
+ };
7269
+ }
7270
+ const command = { id, cmd, cwd, ts: this.now() };
7271
+ const q = this.queue.get(machine) || [];
7272
+ q.push(command);
7273
+ this.queue.set(machine, q);
7274
+ const promise = new Promise((resolve) => {
7275
+ const timer = this.setTimer(() => {
7276
+ if (!this.pending.has(id)) return;
7277
+ this.pending.delete(id);
7278
+ resolve({ id, stdout: "", stderr: `command timed out after ${Math.round(this.execTimeoutMs / 1e3)}s`, exitCode: -1, synthetic: true, ts: this.now() });
7279
+ }, this.execTimeoutMs);
7280
+ this.pending.set(id, { machine, resolve, timer, enqueuedAt: this.now() });
7281
+ });
7282
+ return { id, promise };
7283
+ }
7284
+ /** Long-poll drain: return (and clear) the commands queued for a machine; refreshes liveness. */
7285
+ pollCommands(machine) {
7286
+ this.touch(machine);
7287
+ const q = this.queue.get(machine);
7288
+ if (!q || q.length === 0) return [];
7289
+ this.queue.set(machine, []);
7290
+ return q;
7291
+ }
7292
+ /** Deliver a result from the outpost to the waiting exec. Returns false if unknown/expired. */
7293
+ resolveResult(result) {
7294
+ const p = this.pending.get(result.id);
7295
+ if (!p) return false;
7296
+ this.clearTimer(p.timer);
7297
+ this.pending.delete(result.id);
7298
+ this.touch(p.machine);
7299
+ p.resolve({
7300
+ id: result.id,
7301
+ stdout: result.stdout ?? "",
7302
+ stderr: result.stderr ?? "",
7303
+ exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
7304
+ ts: this.now()
7305
+ });
7306
+ return true;
7307
+ }
7308
+ /** Test/introspection: how many execs are awaiting a result. */
7309
+ pendingCount() {
7310
+ return this.pending.size;
7311
+ }
7312
+ }
7313
+
7110
7314
  const PARTICIPANTS_CHANNEL_ID = "sys-participants";
7111
7315
  function channelPublicView(c) {
7112
7316
  const u = c.upload;
@@ -7513,8 +7717,28 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7513
7717
  const channelStore = new ChannelStore(initialMetadata.path);
7514
7718
  const meetingActivity = [];
7515
7719
  const channelOutbox = new ChannelOutbox(initialMetadata.path);
7720
+ const outpostCoordinator = new OutpostCoordinator();
7721
+ const outpostProjectDir = () => metadata.path || process.cwd();
7722
+ const announceOutpost = (conn) => {
7723
+ const who = conn.label || conn.machine;
7724
+ const plat = [conn.os, conn.arch].filter(Boolean).join("/") || "unknown platform";
7725
+ const msg = {
7726
+ messageId: "op_" + Math.random().toString(16).slice(2, 10),
7727
+ body: `A remote machine connected as an outpost: ${xmlEscape(who)} (${xmlEscape(plat)}). You can now run shell commands on it \u2014 its output returns synchronously, like a local command. Run one with: svamp outpost exec "uname -a"`,
7728
+ timestamp: Date.now(),
7729
+ read: false,
7730
+ from: "outpost",
7731
+ subject: `Remote outpost connected: ${who}`
7732
+ };
7733
+ inbox.push(msg);
7734
+ while (inbox.length > INBOX_MAX) inbox.shift();
7735
+ syncInboxToMetadata();
7736
+ callbacks.onInboxMessage?.(msg);
7737
+ notifyListeners({ type: "inbox-update", sessionId, message: msg });
7738
+ };
7516
7739
  const cfg = server?.config || {};
7517
7740
  const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
7741
+ const outpostServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:outpost` : void 0;
7518
7742
  const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
7519
7743
  const skillCtxFor = (c) => ({
7520
7744
  channelsServiceId,
@@ -7898,34 +8122,36 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7898
8122
  syncChannelsToMetadata();
7899
8123
  return { success: !!caller, caller };
7900
8124
  },
7901
- // FDE outpost (#0589): provision (idempotently) this session's outpost channel and return the
7902
- // install one-liners + direct binary download links + a best-effort connected count. The app's
7903
- // "Connect a remote machine" panel calls this; it reuses the SAME setupOutpostChannel core as
7904
- // `svamp outpost up`, so the channel shape + install command never drift. Admin-gated: the
7905
- // returned token is a capability to run commands on whatever machine connects.
8125
+ // FDE outpost (#0565 redesign): provision (idempotently) this session's durable outpost TOKEN
8126
+ // and return the install one-liners + direct binary download links + a LIVE connected count.
8127
+ // The app's "Connect a remote machine" panel calls this; it reuses the SAME setupOutpost core as
8128
+ // `svamp outpost up`, so the install command never drifts. NO channel is created — the remote
8129
+ // dials the machine's public `:outpost` gateway and this session's token authenticates it.
8130
+ // Admin-gated: the returned token is a capability to run commands on whatever machine connects.
7906
8131
  outpostConnectInfo: async (params, context) => {
7907
8132
  authorizeRequest(context, metadata.sharing, "admin");
7908
- if (!channelsServiceId) return { error: "the channels service is not available on this machine" };
8133
+ if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
7909
8134
  try {
7910
- const info = setupOutpostChannel({
8135
+ const connectedCount = outpostCoordinator.listConnections().length;
8136
+ const info = setupOutpost({
7911
8137
  session: sessionId,
7912
- projectDir: metadata.path || process.cwd(),
7913
- channelsServiceId,
8138
+ projectDir: outpostProjectDir(),
8139
+ outpostServiceId,
7914
8140
  serverUrl: channelsBaseUrl,
7915
- downloadBase: params?.downloadBase || process.env.SVAMP_OUTPOST_DL
8141
+ downloadBase: params?.downloadBase || process.env.SVAMP_OUTPOST_DL,
8142
+ connectedCount
7916
8143
  });
7917
- syncChannelsToMetadata();
7918
8144
  let pairing = {};
7919
8145
  try {
7920
8146
  const am = await server.getService("public/artifact-manager");
7921
8147
  if (am) {
7922
- if (info.connectedCount > 0) {
8148
+ if (connectedCount > 0) {
7923
8149
  await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
7924
8150
  } else {
7925
8151
  const r = await mintPairingCode({
7926
8152
  am,
7927
8153
  session: sessionId,
7928
- payload: { server: info.serverBase, channel: info.channelId, token: info.token, session: sessionId },
8154
+ payload: { server: info.serverBase, token: info.token, session: sessionId },
7929
8155
  resolverBase: channelsBaseUrl,
7930
8156
  log: (m) => console.log(m)
7931
8157
  });
@@ -7940,6 +8166,69 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7940
8166
  return { error: e?.message || String(e) };
7941
8167
  }
7942
8168
  },
8169
+ // ── FDE outpost transport (#0565): token-authed remote endpoints + owner-facing exec/list ──
8170
+ // hello/poll/result carry NO session-sharing auth (the remote is anonymous); the per-session
8171
+ // capability token IS the auth (checked against the durable on-disk token). Routed here by the
8172
+ // machine's public `:outpost` gateway, which passes the remote-supplied `session` as the key.
8173
+ outpostHello: async (params) => {
8174
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8175
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8176
+ const machine = (String(params?.machine || "").trim() || "outpost").slice(0, 128);
8177
+ const { isNew, connection } = outpostCoordinator.registerConnection(machine, {
8178
+ label: params?.label ? String(params.label).slice(0, 128) : void 0,
8179
+ os: params?.os ? String(params.os).slice(0, 64) : void 0,
8180
+ arch: params?.arch ? String(params.arch).slice(0, 64) : void 0,
8181
+ version: params?.version ? String(params.version).slice(0, 64) : void 0
8182
+ });
8183
+ if (isNew) {
8184
+ announceOutpost(connection);
8185
+ try {
8186
+ const am = await server.getService("public/artifact-manager");
8187
+ if (am) await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
8188
+ } catch {
8189
+ }
8190
+ }
8191
+ return { ok: true, machine: connection.machine };
8192
+ },
8193
+ outpostPoll: async (params) => {
8194
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8195
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8196
+ const machine = (String(params?.machine || "").trim() || "outpost").slice(0, 128);
8197
+ if (!outpostCoordinator.isConnected(machine)) {
8198
+ const { isNew, connection } = outpostCoordinator.registerConnection(machine, {});
8199
+ if (isNew) announceOutpost(connection);
8200
+ }
8201
+ const waitRaw = Number(params?.wait ?? 25);
8202
+ const waitSec = Number.isFinite(waitRaw) ? waitRaw : 25;
8203
+ const waitMs = Math.min(Math.max(0, waitSec * 1e3), 6e4);
8204
+ const deadline = Date.now() + waitMs;
8205
+ for (; ; ) {
8206
+ const commands = outpostCoordinator.pollCommands(machine);
8207
+ if (commands.length || Date.now() >= deadline) return { ok: true, commands };
8208
+ await new Promise((resolve) => setTimeout(resolve, 300));
8209
+ }
8210
+ },
8211
+ outpostResult: async (params) => {
8212
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8213
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8214
+ if (!params?.id) return { error: "missing result id" };
8215
+ const ok = outpostCoordinator.resolveResult({ id: params.id, stdout: params.stdout, stderr: params.stderr, exitCode: params.exitCode });
8216
+ return { ok };
8217
+ },
8218
+ outpostExec: async (params, context) => {
8219
+ authorizeRequest(context, metadata.sharing, "admin");
8220
+ const cmd = String(params?.cmd || "").trim();
8221
+ if (!cmd) return { error: "cmd is required" };
8222
+ const t = outpostCoordinator.resolveTarget(params?.target ? String(params.target) : void 0);
8223
+ if ("error" in t) return { error: t.error };
8224
+ const { promise } = outpostCoordinator.enqueueExec(t.machine, cmd, params?.cwd ? String(params.cwd) : void 0);
8225
+ const r = await promise;
8226
+ return { ok: r.exitCode === 0, stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode, machine: t.machine };
8227
+ },
8228
+ outpostList: async (context) => {
8229
+ authorizeRequest(context, metadata.sharing, "view");
8230
+ return { ok: true, outposts: outpostCoordinator.listConnections() };
8231
+ },
7943
8232
  getChannelSkill: async (id, context) => {
7944
8233
  authorizeRequest(context, metadata.sharing, "view");
7945
8234
  const c = channelStore.get(id);
@@ -16693,7 +16982,7 @@ async function startDaemon(options) {
16693
16982
  try {
16694
16983
  const dir = loadSessionIndex()[sessionId]?.directory;
16695
16984
  if (!dir) return;
16696
- const { reconcileServiceLinks } = await import('./agentCommands-C56-TSwf.mjs');
16985
+ const { reconcileServiceLinks } = await import('./agentCommands-BCkWdFFW.mjs');
16697
16986
  const configPath = getSvampConfigPath(dir, sessionId);
16698
16987
  const config = readSvampConfig(configPath);
16699
16988
  const entries = Array.from(urls.entries());
@@ -16711,7 +17000,7 @@ async function startDaemon(options) {
16711
17000
  }
16712
17001
  }
16713
17002
  async function createExposedTunnel(spec) {
16714
- const { FrpcTunnel } = await import('./frpc-Bx0J5hIR.mjs');
17003
+ const { FrpcTunnel } = await import('./frpc-Cb6jUvED.mjs');
16715
17004
  const tunnel = new FrpcTunnel({
16716
17005
  name: spec.name,
16717
17006
  ports: spec.ports,
@@ -18927,11 +19216,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
18927
19216
  });
18928
19217
  },
18929
19218
  onIssue: async (params) => {
18930
- const { issueRpc } = await import('./rpc-C2l4b6gk.mjs');
19219
+ const { issueRpc } = await import('./rpc-B-bxS-Pv.mjs');
18931
19220
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
18932
19221
  },
18933
19222
  onWorkflow: async (params) => {
18934
- const { workflowRpc } = await import('./rpc-D1Vg26a1.mjs');
19223
+ const { workflowRpc } = await import('./rpc-CNkfUlz1.mjs');
18935
19224
  return workflowRpc(params?.cwd || directory, params || {});
18936
19225
  },
18937
19226
  onRipgrep: async (args, cwd) => {
@@ -19544,11 +19833,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19544
19833
  });
19545
19834
  },
19546
19835
  onIssue: async (params) => {
19547
- const { issueRpc } = await import('./rpc-C2l4b6gk.mjs');
19836
+ const { issueRpc } = await import('./rpc-B-bxS-Pv.mjs');
19548
19837
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
19549
19838
  },
19550
19839
  onWorkflow: async (params) => {
19551
- const { workflowRpc } = await import('./rpc-D1Vg26a1.mjs');
19840
+ const { workflowRpc } = await import('./rpc-CNkfUlz1.mjs');
19552
19841
  return workflowRpc(params?.cwd || directory, params || {});
19553
19842
  },
19554
19843
  onRipgrep: async (args, cwd) => {
@@ -20683,7 +20972,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
20683
20972
  const PING_TIMEOUT_MS = 15e3;
20684
20973
  const POST_RECONNECT_GRACE_MS = 2e4;
20685
20974
  const RECONNECT_JITTER_MS = 2500;
20686
- const { WorkflowScheduler } = await import('./scheduler-CGmlf3VQ.mjs');
20975
+ const { WorkflowScheduler } = await import('./scheduler-DT1_BBdZ.mjs');
20687
20976
  const workflowScheduler = new WorkflowScheduler({
20688
20977
  projectRoots: () => {
20689
20978
  const dirs = /* @__PURE__ */ new Set();
@@ -21307,4 +21596,4 @@ var run = /*#__PURE__*/Object.freeze({
21307
21596
  writeStopMarker: writeStopMarker
21308
21597
  });
21309
21598
 
21310
- export { searchSkills as $, removeWorkflow as A, saveWorkflow as B, rawWorkflow as C, listWorkflows as D, isWorkflowEnabled as E, workflowCrons as F, cronMatches as G, buildOutpostInstall as H, ChannelStore as I, DEFAULT_OUTPOST_DOWNLOAD_BASE as J, setupOutpostChannel as K, summarize as L, workflowSteps as M, parseJwtEmail as N, computeCollectionConfigUpdate as O, SYSTEM_COLLECTION_CONFIG as P, loadMachineContext as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, buildMachineInstructions as T, machineToolsForRole as U, buildMachineTools as V, parseFrontmatter as W, getSkillsServer as X, getSkillsWorkspaceName as Y, getSkillsCollectionName as Z, fetchWithTimeout as _, createSessionStore as a, SKILLS_DIR as a0, getSkillInfo as a1, downloadSkillFile as a2, listSkillFiles as a3, resolveModel as a4, clearStopMarker as a5, stopMarkerExists as a6, formatHandle as a7, normalizeAllowedUser as a8, loadSecurityContextConfig as a9, api as aA, run as aB, resolveSecurityContext as aa, buildSecurityContextFromFlags as ab, mergeSecurityContexts as ac, buildSessionShareUrl as ad, computeOutboundHop as ae, registerAwaitingReply as af, buildMachineShareUrl as ag, parseHandle as ah, handleMatchesMetadata as ai, describeMisconfiguration as aj, buildMachineDeps as ak, applyClaudeProxyEnv as al, composeSessionId as am, generateFriendlyName as an, generateHookSettings as ao, staticFileServer as ap, instanceConfig as aq, claudeAuth as ar, codexProvider as as, outbox as at, projectInfo as au, DefaultTransport$1 as av, acpBackend as aw, acpAgentConfig as ax, codexAppServerBackend as ay, GeminiTransport$1 as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, searchIssues as q, registerMachineService as r, startDaemon as s, isVisibleTo as t, updateIssue as u, getRun as v, listRuns as w, getWorkflow as x, runWorkflow as y, setWorkflowEnabled as z };
21599
+ export { listSkillFiles as $, removeWorkflow as A, saveWorkflow as B, rawWorkflow as C, listWorkflows as D, isWorkflowEnabled as E, workflowCrons as F, cronMatches as G, summarize as H, workflowSteps as I, parseJwtEmail as J, computeCollectionConfigUpdate as K, SYSTEM_COLLECTION_CONFIG as L, loadMachineContext as M, buildMachineInstructions as N, machineToolsForRole as O, buildMachineTools as P, parseFrontmatter as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, getSkillsServer as T, getSkillsWorkspaceName as U, getSkillsCollectionName as V, fetchWithTimeout as W, searchSkills as X, SKILLS_DIR as Y, getSkillInfo as Z, downloadSkillFile as _, createSessionStore as a, resolveModel as a0, clearStopMarker as a1, stopMarkerExists as a2, formatHandle as a3, normalizeAllowedUser as a4, loadSecurityContextConfig as a5, resolveSecurityContext as a6, buildSecurityContextFromFlags as a7, mergeSecurityContexts as a8, buildSessionShareUrl as a9, computeOutboundHop as aa, registerAwaitingReply as ab, buildMachineShareUrl as ac, parseHandle as ad, handleMatchesMetadata as ae, describeMisconfiguration as af, buildMachineDeps as ag, applyClaudeProxyEnv as ah, composeSessionId as ai, generateFriendlyName as aj, generateHookSettings as ak, staticFileServer as al, instanceConfig as am, claudeAuth as an, codexProvider as ao, projectInfo as ap, DefaultTransport$1 as aq, acpBackend as ar, acpAgentConfig as as, codexAppServerBackend as at, GeminiTransport$1 as au, api as av, run as aw, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, searchIssues as q, registerMachineService as r, startDaemon as s, isVisibleTo as t, updateIssue as u, getRun as v, listRuns as w, getWorkflow as x, runWorkflow as y, setWorkflowEnabled as z };
@@ -1,4 +1,4 @@
1
- import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { al as applyClaudeProxyEnv, am as composeSessionId, an as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, ao as generateHookSettings } from './run-DrG6nwGm.mjs';
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { ah as applyClaudeProxyEnv, ai as composeSessionId, aj as generateFriendlyName, c as connectToHypha, a as createSessionStore, r as registerMachineService, ak as generateHookSettings } from './run-C0GhrtVf.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,4 +1,4 @@
1
- import { j as resolveProjectRoot, D as listWorkflows, E as isWorkflowEnabled, F as workflowCrons, y as runWorkflow, G as cronMatches } from './run-DrG6nwGm.mjs';
1
+ import { j as resolveProjectRoot, D as listWorkflows, E as isWorkflowEnabled, F as workflowCrons, y as runWorkflow, G as cronMatches } from './run-C0GhrtVf.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-Cb00JySn.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-BAlaK67Y.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
@@ -93,7 +93,7 @@ async function serveAdd(args, machineId) {
93
93
  }
94
94
  }
95
95
  async function serveApply(args, machineId) {
96
- const { connectAndGetMachine } = await import('./commands-Cb00JySn.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-BAlaK67Y.mjs');
97
97
  const fs = await import('fs');
98
98
  const yaml = await import('yaml');
99
99
  const file = positionalArgs(args)[0];
@@ -182,7 +182,7 @@ async function serveApply(args, machineId) {
182
182
  }
183
183
  }
184
184
  async function serveRemove(args, machineId) {
185
- const { connectAndGetMachine } = await import('./commands-Cb00JySn.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-BAlaK67Y.mjs');
186
186
  const pos = positionalArgs(args);
187
187
  const name = pos[0];
188
188
  if (!name) {
@@ -202,7 +202,7 @@ async function serveRemove(args, machineId) {
202
202
  }
203
203
  }
204
204
  async function serveList(args, machineId) {
205
- const { connectAndGetMachine } = await import('./commands-Cb00JySn.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-BAlaK67Y.mjs');
206
206
  const all = hasFlag(args, "--all", "-a");
207
207
  const json = hasFlag(args, "--json");
208
208
  const sessionId = getFlag(args, "--session");
@@ -235,7 +235,7 @@ async function serveList(args, machineId) {
235
235
  }
236
236
  }
237
237
  async function serveInfo(machineId) {
238
- const { connectAndGetMachine } = await import('./commands-Cb00JySn.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-BAlaK67Y.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -1,4 +1,4 @@
1
- import { R as READ_ONLY_TOOLS, Q as loadMachineContext, T as buildMachineInstructions, U as machineToolsForRole, V as buildMachineTools } from './run-DrG6nwGm.mjs';
1
+ import { R as READ_ONLY_TOOLS, M as loadMachineContext, N as buildMachineInstructions, O as machineToolsForRole, P as buildMachineTools } from './run-C0GhrtVf.mjs';
2
2
  import 'node:child_process';
3
3
  import 'os';
4
4
  import 'fs/promises';