svamp-cli 0.2.300 → 0.2.302

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-DS0S7m5p.mjs} +1 -1
  2. package/dist/{agentCommands-C56-TSwf.mjs → agentCommands-DVqrplvE.mjs} +5 -5
  3. package/dist/{auth-y5EinW_c.mjs → auth-BfG-xU5r.mjs} +1 -1
  4. package/dist/{cli-BbbzgMWX.mjs → cli-LhsJPCvo.mjs} +69 -69
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-Db1SMhcL.mjs → commands-42n-Wsc9.mjs} +2 -2
  7. package/dist/{commands-Bjbq2pJg.mjs → commands-BKtoVB7x.mjs} +2 -2
  8. package/dist/{commands-BLFj8bE8.mjs → commands-BTz8U0zj.mjs} +1 -1
  9. package/dist/{commands-Qz7oTXdj.mjs → commands-CPX3zxJe.mjs} +1 -1
  10. package/dist/{commands-Cb00JySn.mjs → commands-CZ_Cyoz7.mjs} +1 -1
  11. package/dist/{commands-BlVHxX-n.mjs → commands-D8ZNPhRo.mjs} +7 -7
  12. package/dist/{commands-VtqFEtFc.mjs → commands-DVGzSbt1.mjs} +2 -2
  13. package/dist/commands-DfbZae0h.mjs +161 -0
  14. package/dist/{fleet-DKAN08u-.mjs → fleet-CiHBV4mW.mjs} +1 -1
  15. package/dist/{frpc-Bx0J5hIR.mjs → frpc-4f18mvfV.mjs} +1 -1
  16. package/dist/{headlessCli-DaqkPy9x.mjs → headlessCli-Dl3YG5R3.mjs} +2 -2
  17. package/dist/index.mjs +1 -1
  18. package/dist/{notifyCommands-DaAd67p4.mjs → notifyCommands-BqYtOHL3.mjs} +1 -1
  19. package/dist/package-DPWB-cK9.mjs +64 -0
  20. package/dist/{rpc-C2l4b6gk.mjs → rpc-BRt7OvrH.mjs} +1 -1
  21. package/dist/{rpc-D1Vg26a1.mjs → rpc-DaeHWgwG.mjs} +1 -1
  22. package/dist/{run-DrG6nwGm.mjs → run-BCS8Ckf7.mjs} +379 -61
  23. package/dist/{run-CVRu9pzU.mjs → run-BijIGfNu.mjs} +1 -1
  24. package/dist/{scheduler-CGmlf3VQ.mjs → scheduler-DobkEDw0.mjs} +1 -1
  25. package/dist/{serveCommands-CiYytaPO.mjs → serveCommands-0F39YIXd.mjs} +5 -5
  26. package/dist/{sideband-BQz9FNc9.mjs → sideband-C9Y7_Sni.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-4f18mvfV.mjs');
2977
2977
  let tunnel;
2978
2978
  tunnel = new FrpcTunnel({
2979
2979
  name: tunnelName,
@@ -3274,12 +3274,28 @@ function codexForcedModel(env = process.env) {
3274
3274
  if (!f || f === "0" || f === "false" || f === "no" || f === "off") return void 0;
3275
3275
  return env.SVAMP_CODEX_MODEL || env.LLM_MODEL || void 0;
3276
3276
  }
3277
+ function codexModelMenu(env = process.env) {
3278
+ return (env.SVAMP_CODEX_MODELS || "").split(",").map((s) => s.trim()).filter(Boolean).filter((m, i, a) => a.indexOf(m) === i);
3279
+ }
3280
+ function codexDefaultModel(env = process.env) {
3281
+ const menu = codexModelMenu(env);
3282
+ return env.SVAMP_CODEX_MODEL || env.LLM_MODEL || menu[0] || void 0;
3283
+ }
3284
+ function codexClampModel(requested, env = process.env) {
3285
+ const menu = codexModelMenu(env);
3286
+ if (!menu.length) return requested;
3287
+ if (requested && menu.includes(requested)) return requested;
3288
+ return codexDefaultModel(env);
3289
+ }
3277
3290
 
3278
3291
  var codexProvider = /*#__PURE__*/Object.freeze({
3279
3292
  __proto__: null,
3280
3293
  CODEX_PROVIDER_KEY_ENV: CODEX_PROVIDER_KEY_ENV,
3281
3294
  buildCodexProviderArgs: buildCodexProviderArgs,
3295
+ codexClampModel: codexClampModel,
3296
+ codexDefaultModel: codexDefaultModel,
3282
3297
  codexForcedModel: codexForcedModel,
3298
+ codexModelMenu: codexModelMenu,
3283
3299
  codexPermissionSettings: codexPermissionSettings,
3284
3300
  hasCustomProvider: hasCustomProvider,
3285
3301
  resolveCodexProvider: resolveCodexProvider
@@ -4639,11 +4655,6 @@ class ChannelOutbox {
4639
4655
  }
4640
4656
  }
4641
4657
 
4642
- var outbox = /*#__PURE__*/Object.freeze({
4643
- __proto__: null,
4644
- ChannelOutbox: ChannelOutbox
4645
- });
4646
-
4647
4658
  function getParamNames(fn) {
4648
4659
  const src = fn.toString();
4649
4660
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -5872,7 +5883,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
5872
5883
  const tunnels = handlers.tunnels;
5873
5884
  if (!tunnels) throw new Error("Tunnel management not available");
5874
5885
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
5875
- const { FrpcTunnel } = await import('./frpc-Bx0J5hIR.mjs');
5886
+ const { FrpcTunnel } = await import('./frpc-4f18mvfV.mjs');
5876
5887
  const tunnel = new FrpcTunnel({
5877
5888
  name: params.name,
5878
5889
  ports: params.ports,
@@ -6348,7 +6359,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6348
6359
  }
6349
6360
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
6350
6361
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
6351
- const { toolsForRole } = await import('./sideband-BQz9FNc9.mjs');
6362
+ const { toolsForRole } = await import('./sideband-C9Y7_Sni.mjs');
6352
6363
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
6353
6364
  return fmt(r2);
6354
6365
  }
@@ -6453,7 +6464,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
6453
6464
  return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
6454
6465
  }
6455
6466
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
6456
- const { queryCore } = await import('./commands-Cb00JySn.mjs');
6467
+ const { queryCore } = await import('./commands-CZ_Cyoz7.mjs');
6457
6468
  const timeout = c.reply?.timeout_sec || 120;
6458
6469
  let result;
6459
6470
  try {
@@ -6605,6 +6616,44 @@ ${d?.error || "not found"}`;
6605
6616
  { overwrite: true }
6606
6617
  );
6607
6618
  console.log(`[HYPHA MACHINE] Channels service registered: ${channelsServiceInfo.id} (type svamp-channels)`);
6619
+ 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>"}}.';
6620
+ const routeOutpost = (session) => {
6621
+ const sid = String(session || "").trim();
6622
+ if (!sid) return { error: OUTPOST_MISSING_SESSION };
6623
+ const rpc = handlers.getSessionRPCHandlers?.(sid);
6624
+ if (!rpc?.outpostHello) return { error: `session ${sid.slice(0, 12)} is not connected on this machine` };
6625
+ return { rpc };
6626
+ };
6627
+ const outpostServiceInfo = await server.registerService(
6628
+ {
6629
+ id: "outpost",
6630
+ name: `Svamp Outpost (${currentMetadata.displayName || machineId})`,
6631
+ type: "svamp-outpost",
6632
+ // Public so a remote machine (possibly anonymous) can dial in; token-authed per call,
6633
+ // so no Hypha identity/context is needed (the handlers ignore context entirely).
6634
+ config: { visibility: "public", require_context: false },
6635
+ hello: async (kwargs = {}) => {
6636
+ trackInbound();
6637
+ const r = routeOutpost(kwargs?.session);
6638
+ if ("error" in r) return { error: r.error };
6639
+ return r.rpc.outpostHello(kwargs);
6640
+ },
6641
+ poll: async (kwargs = {}) => {
6642
+ trackInbound();
6643
+ const r = routeOutpost(kwargs?.session);
6644
+ if ("error" in r) return { error: r.error };
6645
+ return r.rpc.outpostPoll(kwargs);
6646
+ },
6647
+ result: async (kwargs = {}) => {
6648
+ trackInbound();
6649
+ const r = routeOutpost(kwargs?.session);
6650
+ if ("error" in r) return { error: r.error };
6651
+ return r.rpc.outpostResult(kwargs);
6652
+ }
6653
+ },
6654
+ { overwrite: true }
6655
+ );
6656
+ console.log(`[HYPHA MACHINE] Outpost service registered: ${outpostServiceInfo.id} (type svamp-outpost)`);
6608
6657
  return {
6609
6658
  serviceInfo,
6610
6659
  notifySessionEvent: notifyListeners,
@@ -6639,6 +6688,8 @@ ${d?.error || "not found"}`;
6639
6688
  await server.unregisterService(serviceInfo.id);
6640
6689
  await server.unregisterService(channelsServiceInfo.id).catch(() => {
6641
6690
  });
6691
+ await server.unregisterService(outpostServiceInfo.id).catch(() => {
6692
+ });
6642
6693
  }
6643
6694
  };
6644
6695
  }
@@ -6930,6 +6981,43 @@ function retryDelayMs(now, backoffUntil) {
6930
6981
  return Math.max(0, backoffUntil - now);
6931
6982
  }
6932
6983
 
6984
+ function tokenPath(projectDir, session) {
6985
+ return join$1(projectDir, ".svamp", session, "outpost.json");
6986
+ }
6987
+ function genToken() {
6988
+ return randomBytes(24).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6989
+ }
6990
+ function loadOutpostToken(projectDir, session) {
6991
+ const file = tokenPath(projectDir, session);
6992
+ if (!existsSync(file)) return null;
6993
+ try {
6994
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
6995
+ if (!parsed || typeof parsed.token !== "string" || !parsed.token) return null;
6996
+ return parsed;
6997
+ } catch {
6998
+ return null;
6999
+ }
7000
+ }
7001
+ function ensureOutpostToken(projectDir, session) {
7002
+ const existing = loadOutpostToken(projectDir, session);
7003
+ if (existing) return existing;
7004
+ const rec = { version: 1, session, token: genToken(), createdAt: Date.now() };
7005
+ const file = tokenPath(projectDir, session);
7006
+ mkdirSync$1(dirname(file), { recursive: true });
7007
+ writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
7008
+ try {
7009
+ chmodSync(file, 384);
7010
+ } catch {
7011
+ }
7012
+ return rec;
7013
+ }
7014
+ function outpostTokenMatches(rec, presented) {
7015
+ if (!rec || typeof presented !== "string" || presented.length !== rec.token.length) return false;
7016
+ let diff = 0;
7017
+ for (let i = 0; i < rec.token.length; i++) diff |= rec.token.charCodeAt(i) ^ presented.charCodeAt(i);
7018
+ return diff === 0;
7019
+ }
7020
+
6933
7021
  const DEFAULT_DOWNLOAD_BASE = "https://github.com/amun-ai/hypha-cloud/releases/latest/download";
6934
7022
  function shq(v) {
6935
7023
  return `'${String(v).replace(/'/g, `'\\''`)}'`;
@@ -6940,10 +7028,10 @@ function psq(v) {
6940
7028
  function buildOutpostInstall(params) {
6941
7029
  const base = params.serverBase.replace(/\/$/, "");
6942
7030
  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)}`;
7031
+ const session = params.session;
7032
+ 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`;
7033
+ 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`;
7034
+ const direct = `svamp-outpost --server ${shq(base)} --token ${shq(params.token)} --session ${shq(session)}`;
6947
7035
  return { unix, windows, direct };
6948
7036
  }
6949
7037
 
@@ -6955,37 +7043,19 @@ const OUTPOST_BINARIES = [
6955
7043
  { label: "Linux (arm64)", file: "svamp-outpost-linux-arm64" },
6956
7044
  { label: "Windows (x86-64)", file: "svamp-outpost-windows-amd64.exe" }
6957
7045
  ];
6958
- function setupOutpostChannel(params) {
7046
+ function setupOutpost(params) {
6959
7047
  const serverUrl = params.serverUrl || "https://hypha.aicell.io";
6960
7048
  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;
7049
+ const serverBase = gatewayBase(params.outpostServiceId, serverUrl);
7050
+ const { token } = ensureOutpostToken(params.projectDir, params.session);
7051
+ const install = buildOutpostInstall({ serverBase, session: params.session, token, downloadBase });
6981
7052
  return {
6982
- channelId,
6983
7053
  token,
6984
7054
  serverBase,
6985
7055
  downloadBase,
6986
7056
  install,
6987
7057
  binaries: OUTPOST_BINARIES.map((b) => ({ label: b.label, url: `${downloadBase}/${b.file}` })),
6988
- connectedCount
7058
+ connectedCount: params.connectedCount || 0
6989
7059
  };
6990
7060
  }
6991
7061
 
@@ -7070,7 +7140,6 @@ async function mintPairingCode(opts) {
7070
7140
  const expiresAt = now + ttlMs;
7071
7141
  const pairJson = JSON.stringify({
7072
7142
  server: payload.server,
7073
- channel: payload.channel,
7074
7143
  token: payload.token,
7075
7144
  session: payload.session,
7076
7145
  expiresAt
@@ -7107,6 +7176,157 @@ async function mintPairingCode(opts) {
7107
7176
  return { code, resolver, expiresAt };
7108
7177
  }
7109
7178
 
7179
+ const OUTPOST_STALE_MS = 9e4;
7180
+ const OUTPOST_EXEC_TIMEOUT_MS = 10 * 6e4;
7181
+ class OutpostCoordinator {
7182
+ conns = /* @__PURE__ */ new Map();
7183
+ queue = /* @__PURE__ */ new Map();
7184
+ pending = /* @__PURE__ */ new Map();
7185
+ seq = 0;
7186
+ now;
7187
+ setTimer;
7188
+ clearTimer;
7189
+ execTimeoutMs;
7190
+ staleMs;
7191
+ constructor(opts = {}) {
7192
+ this.now = opts.now || (() => Date.now());
7193
+ this.setTimer = opts.setTimer || ((fn, ms) => setTimeout(fn, ms));
7194
+ this.clearTimer = opts.clearTimer || ((t) => clearTimeout(t));
7195
+ this.execTimeoutMs = opts.execTimeoutMs ?? OUTPOST_EXEC_TIMEOUT_MS;
7196
+ this.staleMs = opts.staleMs ?? OUTPOST_STALE_MS;
7197
+ }
7198
+ isStale(c, at) {
7199
+ return at - c.lastSeen > this.staleMs;
7200
+ }
7201
+ /** Drop dead connections; fail any in-flight execs bound to them so callers never hang. */
7202
+ prune(at = this.now()) {
7203
+ for (const [machine, c] of [...this.conns]) {
7204
+ if (this.isStale(c, at)) {
7205
+ this.conns.delete(machine);
7206
+ this.queue.delete(machine);
7207
+ this.failMachine(machine, "the remote outpost disconnected");
7208
+ }
7209
+ }
7210
+ }
7211
+ /** Reject every pending exec on a machine with a synthetic disconnect result (exitCode -1). */
7212
+ failMachine(machine, reason) {
7213
+ for (const [id, p] of [...this.pending]) {
7214
+ if (p.machine !== machine) continue;
7215
+ this.clearTimer(p.timer);
7216
+ this.pending.delete(id);
7217
+ p.resolve({ id, stdout: "", stderr: reason, exitCode: -1, synthetic: true, ts: this.now() });
7218
+ }
7219
+ }
7220
+ /**
7221
+ * A remote said hello (first connect OR reconnect after an outage). Upserts the connection and
7222
+ * returns `isNew` = true when it should trigger the ONE-TIME inbox announce — true on the first
7223
+ * appearance and on a reconnect after the connection had gone stale/pruned.
7224
+ */
7225
+ registerConnection(machine, meta = {}) {
7226
+ const at = this.now();
7227
+ this.prune(at);
7228
+ const existing = this.conns.get(machine);
7229
+ const isNew = !existing;
7230
+ const connection = {
7231
+ machine,
7232
+ label: meta.label ?? existing?.label,
7233
+ os: meta.os ?? existing?.os,
7234
+ arch: meta.arch ?? existing?.arch,
7235
+ version: meta.version ?? existing?.version,
7236
+ connectedAt: existing?.connectedAt ?? at,
7237
+ lastSeen: at
7238
+ };
7239
+ this.conns.set(machine, connection);
7240
+ return { isNew, connection };
7241
+ }
7242
+ /** Refresh liveness for an active poll (keeps the connection "always on"). */
7243
+ touch(machine) {
7244
+ const c = this.conns.get(machine);
7245
+ if (!c) return false;
7246
+ c.lastSeen = this.now();
7247
+ return true;
7248
+ }
7249
+ /** Live connections only (prunes first). */
7250
+ listConnections() {
7251
+ this.prune();
7252
+ return [...this.conns.values()].sort((a, b) => a.connectedAt - b.connectedAt);
7253
+ }
7254
+ isConnected(machine) {
7255
+ this.prune();
7256
+ return this.conns.has(machine);
7257
+ }
7258
+ /**
7259
+ * Pick the machine an exec should target: an explicit live target, or the sole live outpost.
7260
+ * Ambiguity (0 or >1 without a target) is a clear error, never a silent guess.
7261
+ */
7262
+ resolveTarget(target) {
7263
+ const live = this.listConnections();
7264
+ if (target) {
7265
+ const hit = live.find((c) => c.machine === target || c.label === target);
7266
+ if (!hit) return { error: `no connected outpost named "${target}" (connected: ${live.map((c) => c.machine).join(", ") || "none"})` };
7267
+ return { machine: hit.machine };
7268
+ }
7269
+ if (live.length === 0) return { error: "no remote outpost is connected \u2014 run the install one-liner on the remote machine first" };
7270
+ if (live.length > 1) return { error: `multiple outposts connected (${live.map((c) => c.machine).join(", ")}) \u2014 pass a target` };
7271
+ return { machine: live[0].machine };
7272
+ }
7273
+ /**
7274
+ * Enqueue a command for a machine and return a promise that resolves with the REAL result once
7275
+ * the outpost posts it back (or a synthetic exitCode -1 on disconnect/timeout). Fails fast if the
7276
+ * machine is not connected.
7277
+ */
7278
+ enqueueExec(machine, cmd, cwd) {
7279
+ const id = `x${(++this.seq).toString(36)}_${this.now().toString(36)}`;
7280
+ if (!this.isConnected(machine)) {
7281
+ return {
7282
+ id,
7283
+ promise: Promise.resolve({ id, stdout: "", stderr: `outpost "${machine}" is not connected`, exitCode: -1, synthetic: true, ts: this.now() })
7284
+ };
7285
+ }
7286
+ const command = { id, cmd, cwd, ts: this.now() };
7287
+ const q = this.queue.get(machine) || [];
7288
+ q.push(command);
7289
+ this.queue.set(machine, q);
7290
+ const promise = new Promise((resolve) => {
7291
+ const timer = this.setTimer(() => {
7292
+ if (!this.pending.has(id)) return;
7293
+ this.pending.delete(id);
7294
+ resolve({ id, stdout: "", stderr: `command timed out after ${Math.round(this.execTimeoutMs / 1e3)}s`, exitCode: -1, synthetic: true, ts: this.now() });
7295
+ }, this.execTimeoutMs);
7296
+ this.pending.set(id, { machine, resolve, timer, enqueuedAt: this.now() });
7297
+ });
7298
+ return { id, promise };
7299
+ }
7300
+ /** Long-poll drain: return (and clear) the commands queued for a machine; refreshes liveness. */
7301
+ pollCommands(machine) {
7302
+ this.touch(machine);
7303
+ const q = this.queue.get(machine);
7304
+ if (!q || q.length === 0) return [];
7305
+ this.queue.set(machine, []);
7306
+ return q;
7307
+ }
7308
+ /** Deliver a result from the outpost to the waiting exec. Returns false if unknown/expired. */
7309
+ resolveResult(result) {
7310
+ const p = this.pending.get(result.id);
7311
+ if (!p) return false;
7312
+ this.clearTimer(p.timer);
7313
+ this.pending.delete(result.id);
7314
+ this.touch(p.machine);
7315
+ p.resolve({
7316
+ id: result.id,
7317
+ stdout: result.stdout ?? "",
7318
+ stderr: result.stderr ?? "",
7319
+ exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
7320
+ ts: this.now()
7321
+ });
7322
+ return true;
7323
+ }
7324
+ /** Test/introspection: how many execs are awaiting a result. */
7325
+ pendingCount() {
7326
+ return this.pending.size;
7327
+ }
7328
+ }
7329
+
7110
7330
  const PARTICIPANTS_CHANNEL_ID = "sys-participants";
7111
7331
  function channelPublicView(c) {
7112
7332
  const u = c.upload;
@@ -7513,8 +7733,28 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7513
7733
  const channelStore = new ChannelStore(initialMetadata.path);
7514
7734
  const meetingActivity = [];
7515
7735
  const channelOutbox = new ChannelOutbox(initialMetadata.path);
7736
+ const outpostCoordinator = new OutpostCoordinator();
7737
+ const outpostProjectDir = () => metadata.path || process.cwd();
7738
+ const announceOutpost = (conn) => {
7739
+ const who = conn.label || conn.machine;
7740
+ const plat = [conn.os, conn.arch].filter(Boolean).join("/") || "unknown platform";
7741
+ const msg = {
7742
+ messageId: "op_" + Math.random().toString(16).slice(2, 10),
7743
+ 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"`,
7744
+ timestamp: Date.now(),
7745
+ read: false,
7746
+ from: "outpost",
7747
+ subject: `Remote outpost connected: ${who}`
7748
+ };
7749
+ inbox.push(msg);
7750
+ while (inbox.length > INBOX_MAX) inbox.shift();
7751
+ syncInboxToMetadata();
7752
+ callbacks.onInboxMessage?.(msg);
7753
+ notifyListeners({ type: "inbox-update", sessionId, message: msg });
7754
+ };
7516
7755
  const cfg = server?.config || {};
7517
7756
  const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
7757
+ const outpostServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:outpost` : void 0;
7518
7758
  const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
7519
7759
  const skillCtxFor = (c) => ({
7520
7760
  channelsServiceId,
@@ -7898,34 +8138,36 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7898
8138
  syncChannelsToMetadata();
7899
8139
  return { success: !!caller, caller };
7900
8140
  },
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.
8141
+ // FDE outpost (#0565 redesign): provision (idempotently) this session's durable outpost TOKEN
8142
+ // and return the install one-liners + direct binary download links + a LIVE connected count.
8143
+ // The app's "Connect a remote machine" panel calls this; it reuses the SAME setupOutpost core as
8144
+ // `svamp outpost up`, so the install command never drifts. NO channel is created — the remote
8145
+ // dials the machine's public `:outpost` gateway and this session's token authenticates it.
8146
+ // Admin-gated: the returned token is a capability to run commands on whatever machine connects.
7906
8147
  outpostConnectInfo: async (params, context) => {
7907
8148
  authorizeRequest(context, metadata.sharing, "admin");
7908
- if (!channelsServiceId) return { error: "the channels service is not available on this machine" };
8149
+ if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
7909
8150
  try {
7910
- const info = setupOutpostChannel({
8151
+ const connectedCount = outpostCoordinator.listConnections().length;
8152
+ const info = setupOutpost({
7911
8153
  session: sessionId,
7912
- projectDir: metadata.path || process.cwd(),
7913
- channelsServiceId,
8154
+ projectDir: outpostProjectDir(),
8155
+ outpostServiceId,
7914
8156
  serverUrl: channelsBaseUrl,
7915
- downloadBase: params?.downloadBase || process.env.SVAMP_OUTPOST_DL
8157
+ downloadBase: params?.downloadBase || process.env.SVAMP_OUTPOST_DL,
8158
+ connectedCount
7916
8159
  });
7917
- syncChannelsToMetadata();
7918
8160
  let pairing = {};
7919
8161
  try {
7920
8162
  const am = await server.getService("public/artifact-manager");
7921
8163
  if (am) {
7922
- if (info.connectedCount > 0) {
8164
+ if (connectedCount > 0) {
7923
8165
  await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
7924
8166
  } else {
7925
8167
  const r = await mintPairingCode({
7926
8168
  am,
7927
8169
  session: sessionId,
7928
- payload: { server: info.serverBase, channel: info.channelId, token: info.token, session: sessionId },
8170
+ payload: { server: info.serverBase, token: info.token, session: sessionId },
7929
8171
  resolverBase: channelsBaseUrl,
7930
8172
  log: (m) => console.log(m)
7931
8173
  });
@@ -7940,6 +8182,69 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
7940
8182
  return { error: e?.message || String(e) };
7941
8183
  }
7942
8184
  },
8185
+ // ── FDE outpost transport (#0565): token-authed remote endpoints + owner-facing exec/list ──
8186
+ // hello/poll/result carry NO session-sharing auth (the remote is anonymous); the per-session
8187
+ // capability token IS the auth (checked against the durable on-disk token). Routed here by the
8188
+ // machine's public `:outpost` gateway, which passes the remote-supplied `session` as the key.
8189
+ outpostHello: async (params) => {
8190
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8191
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8192
+ const machine = (String(params?.machine || "").trim() || "outpost").slice(0, 128);
8193
+ const { isNew, connection } = outpostCoordinator.registerConnection(machine, {
8194
+ label: params?.label ? String(params.label).slice(0, 128) : void 0,
8195
+ os: params?.os ? String(params.os).slice(0, 64) : void 0,
8196
+ arch: params?.arch ? String(params.arch).slice(0, 64) : void 0,
8197
+ version: params?.version ? String(params.version).slice(0, 64) : void 0
8198
+ });
8199
+ if (isNew) {
8200
+ announceOutpost(connection);
8201
+ try {
8202
+ const am = await server.getService("public/artifact-manager");
8203
+ if (am) await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
8204
+ } catch {
8205
+ }
8206
+ }
8207
+ return { ok: true, machine: connection.machine };
8208
+ },
8209
+ outpostPoll: async (params) => {
8210
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8211
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8212
+ const machine = (String(params?.machine || "").trim() || "outpost").slice(0, 128);
8213
+ if (!outpostCoordinator.isConnected(machine)) {
8214
+ const { isNew, connection } = outpostCoordinator.registerConnection(machine, {});
8215
+ if (isNew) announceOutpost(connection);
8216
+ }
8217
+ const waitRaw = Number(params?.wait ?? 25);
8218
+ const waitSec = Number.isFinite(waitRaw) ? waitRaw : 25;
8219
+ const waitMs = Math.min(Math.max(0, waitSec * 1e3), 6e4);
8220
+ const deadline = Date.now() + waitMs;
8221
+ for (; ; ) {
8222
+ const commands = outpostCoordinator.pollCommands(machine);
8223
+ if (commands.length || Date.now() >= deadline) return { ok: true, commands };
8224
+ await new Promise((resolve) => setTimeout(resolve, 300));
8225
+ }
8226
+ },
8227
+ outpostResult: async (params) => {
8228
+ const rec = loadOutpostToken(outpostProjectDir(), sessionId);
8229
+ if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
8230
+ if (!params?.id) return { error: "missing result id" };
8231
+ const ok = outpostCoordinator.resolveResult({ id: params.id, stdout: params.stdout, stderr: params.stderr, exitCode: params.exitCode });
8232
+ return { ok };
8233
+ },
8234
+ outpostExec: async (params, context) => {
8235
+ authorizeRequest(context, metadata.sharing, "admin");
8236
+ const cmd = String(params?.cmd || "").trim();
8237
+ if (!cmd) return { error: "cmd is required" };
8238
+ const t = outpostCoordinator.resolveTarget(params?.target ? String(params.target) : void 0);
8239
+ if ("error" in t) return { error: t.error };
8240
+ const { promise } = outpostCoordinator.enqueueExec(t.machine, cmd, params?.cwd ? String(params.cwd) : void 0);
8241
+ const r = await promise;
8242
+ return { ok: r.exitCode === 0, stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode, machine: t.machine };
8243
+ },
8244
+ outpostList: async (context) => {
8245
+ authorizeRequest(context, metadata.sharing, "view");
8246
+ return { ok: true, outposts: outpostCoordinator.listConnections() };
8247
+ },
7943
8248
  getChannelSkill: async (id, context) => {
7944
8249
  authorizeRequest(context, metadata.sharing, "view");
7945
8250
  const c = channelStore.get(id);
@@ -16693,7 +16998,7 @@ async function startDaemon(options) {
16693
16998
  try {
16694
16999
  const dir = loadSessionIndex()[sessionId]?.directory;
16695
17000
  if (!dir) return;
16696
- const { reconcileServiceLinks } = await import('./agentCommands-C56-TSwf.mjs');
17001
+ const { reconcileServiceLinks } = await import('./agentCommands-DVqrplvE.mjs');
16697
17002
  const configPath = getSvampConfigPath(dir, sessionId);
16698
17003
  const config = readSvampConfig(configPath);
16699
17004
  const entries = Array.from(urls.entries());
@@ -16711,7 +17016,7 @@ async function startDaemon(options) {
16711
17016
  }
16712
17017
  }
16713
17018
  async function createExposedTunnel(spec) {
16714
- const { FrpcTunnel } = await import('./frpc-Bx0J5hIR.mjs');
17019
+ const { FrpcTunnel } = await import('./frpc-4f18mvfV.mjs');
16715
17020
  const tunnel = new FrpcTunnel({
16716
17021
  name: spec.name,
16717
17022
  ports: spec.ports,
@@ -18927,11 +19232,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
18927
19232
  });
18928
19233
  },
18929
19234
  onIssue: async (params) => {
18930
- const { issueRpc } = await import('./rpc-C2l4b6gk.mjs');
19235
+ const { issueRpc } = await import('./rpc-BRt7OvrH.mjs');
18931
19236
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
18932
19237
  },
18933
19238
  onWorkflow: async (params) => {
18934
- const { workflowRpc } = await import('./rpc-D1Vg26a1.mjs');
19239
+ const { workflowRpc } = await import('./rpc-DaeHWgwG.mjs');
18935
19240
  return workflowRpc(params?.cwd || directory, params || {});
18936
19241
  },
18937
19242
  onRipgrep: async (args, cwd) => {
@@ -19304,7 +19609,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19304
19609
  }
19305
19610
  {
19306
19611
  const forcedModel = agentName === "codex" ? codexForcedModel() : void 0;
19307
- const rawModel = forcedModel || (typeof msgMeta?.model === "string" && msgMeta.model ? msgMeta.model : void 0);
19612
+ const composerModel = typeof msgMeta?.model === "string" && msgMeta.model ? msgMeta.model : void 0;
19613
+ const rawModel = forcedModel || (agentName === "codex" && codexModelMenu().length ? codexClampModel(composerModel) : composerModel);
19308
19614
  const desiredModel = rawModel || agentConfig?.default_model || void 0;
19309
19615
  if (desiredModel !== appliedAgentModel) {
19310
19616
  agentBackend?.setModel?.(desiredModel);
@@ -19544,11 +19850,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19544
19850
  });
19545
19851
  },
19546
19852
  onIssue: async (params) => {
19547
- const { issueRpc } = await import('./rpc-C2l4b6gk.mjs');
19853
+ const { issueRpc } = await import('./rpc-BRt7OvrH.mjs');
19548
19854
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
19549
19855
  },
19550
19856
  onWorkflow: async (params) => {
19551
- const { workflowRpc } = await import('./rpc-D1Vg26a1.mjs');
19857
+ const { workflowRpc } = await import('./rpc-DaeHWgwG.mjs');
19552
19858
  return workflowRpc(params?.cwd || directory, params || {});
19553
19859
  },
19554
19860
  onRipgrep: async (args, cwd) => {
@@ -19729,8 +20035,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
19729
20035
  }
19730
20036
  }
19731
20037
  }
19732
- const codexModel = codexForcedModel() || options2.model || agentConfig?.default_model || provider.model || void 0;
19733
- if (codexModel && (provider.apiBase || codexForcedModel())) {
20038
+ const codexMenu = codexModelMenu();
20039
+ const codexModel = codexForcedModel() || (codexMenu.length ? codexClampModel(options2.model ?? void 0) || codexDefaultModel() : void 0) || options2.model || agentConfig?.default_model || provider.model || void 0;
20040
+ if (codexMenu.length) {
20041
+ sessionMetadata.models = codexMenu.map((m) => ({
20042
+ code: m,
20043
+ value: m,
20044
+ description: m === codexModel ? "Default on this machine" : "Available on this machine"
20045
+ }));
20046
+ try {
20047
+ sessionService.updateMetadata(sessionMetadata);
20048
+ } catch {
20049
+ }
20050
+ logger.log(`[Agent Session ${sessionId}] Codex model menu advertised (SVAMP_CODEX_MODELS): [${codexMenu.join(", ")}] default=${codexModel}`);
20051
+ } else if (codexModel && (provider.apiBase || codexForcedModel())) {
19734
20052
  sessionMetadata.models = [{ code: codexModel, value: codexModel, description: "Configured on this machine" }];
19735
20053
  try {
19736
20054
  sessionService.updateMetadata(sessionMetadata);
@@ -20683,7 +21001,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
20683
21001
  const PING_TIMEOUT_MS = 15e3;
20684
21002
  const POST_RECONNECT_GRACE_MS = 2e4;
20685
21003
  const RECONNECT_JITTER_MS = 2500;
20686
- const { WorkflowScheduler } = await import('./scheduler-CGmlf3VQ.mjs');
21004
+ const { WorkflowScheduler } = await import('./scheduler-DobkEDw0.mjs');
20687
21005
  const workflowScheduler = new WorkflowScheduler({
20688
21006
  projectRoots: () => {
20689
21007
  const dirs = /* @__PURE__ */ new Set();
@@ -21307,4 +21625,4 @@ var run = /*#__PURE__*/Object.freeze({
21307
21625
  writeStopMarker: writeStopMarker
21308
21626
  });
21309
21627
 
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 };
21628
+ 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-BCS8Ckf7.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-BCS8Ckf7.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';