svamp-cli 0.2.315 → 0.2.318
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.
- package/bin/commands/workflow.md +3 -2
- package/dist/{adminCommands-DzNor_Oi.mjs → adminCommands-_az3eIOd.mjs} +1 -1
- package/dist/{agentCommands-RhxyIA_z.mjs → agentCommands-CI_sYhy6.mjs} +5 -5
- package/dist/{auth-DwbfJdef.mjs → auth-7Og-RZ50.mjs} +1 -1
- package/dist/{cli-3UOhFKAA.mjs → cli-D3_a7yin.mjs} +115 -85
- package/dist/cli.mjs +2 -2
- package/dist/{commands-DpWQUbS1.mjs → commands-B9niiK4o.mjs} +1 -1
- package/dist/{commands-Clvx4KkJ.mjs → commands-C5bo2-Pk.mjs} +2 -2
- package/dist/{commands-8cNqkP6Q.mjs → commands-CDL2fp9I.mjs} +2 -2
- package/dist/{commands-DOXnuGx3.mjs → commands-CwvpWFl7.mjs} +8 -8
- package/dist/{commands-CjBsJQX-.mjs → commands-DRYk0mQr.mjs} +1 -1
- package/dist/{commands-C5Lmdg3I.mjs → commands-DaYsK7Af.mjs} +3 -3
- package/dist/{commands-BTkUJBuQ.mjs → commands-DxJHm9r0.mjs} +1 -1
- package/dist/{commands-Cfmfo9-g.mjs → commands-eS7P4neN.mjs} +2 -2
- package/dist/{fleet-C9RYmsHH.mjs → fleet-BXfUlehy.mjs} +6 -5
- package/dist/{headlessCli-BoaIinBA.mjs → headlessCli-CR9fW45i.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{notifyCommands-8hUYEN4j.mjs → notifyCommands-DUr9QjLt.mjs} +1 -1
- package/dist/package-CDjOkJPh.mjs +64 -0
- package/dist/{pinnedClaudeCode-mM9Q6Q2G.mjs → pinnedClaudeCode-CnCpensV.mjs} +1 -1
- package/dist/{rpc-DAcODVs3.mjs → rpc-CaKQ3aCj.mjs} +2 -2
- package/dist/{rpc-BFIy8FTe.mjs → rpc-DX7_LzZf.mjs} +1 -1
- package/dist/{run-CkmRGy8q.mjs → run-BuZDMLWU.mjs} +1 -1
- package/dist/{run-vEbmTx6x.mjs → run-CC2ckdDX.mjs} +1893 -553
- package/dist/{scheduler-DlDYY_wS.mjs → scheduler-B11LcOId.mjs} +1 -1
- package/dist/{serveCommands-wx5GnznD.mjs → serveCommands-DtPbaNdu.mjs} +5 -5
- package/dist/{sideband-DU8QZetT.mjs → sideband-C0CGbTIK.mjs} +1 -1
- package/package.json +4 -4
- package/dist/frpc-pwqsONPX.mjs +0 -775
- package/dist/package-D_KxkJI4.mjs +0 -64
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir } from 'os';
|
|
1
|
+
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir, platform, arch } from 'os';
|
|
2
2
|
import fs$1, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
-
import fs__default, { existsSync as existsSync$1, rmSync, readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync as renameSync$1, realpathSync as realpathSync$1, copyFileSync,
|
|
4
|
+
import fs__default, { existsSync as existsSync$1, rmSync, readFileSync as readFileSync$1, mkdirSync, writeFileSync, chmodSync, unlinkSync, renameSync as renameSync$1, realpathSync as realpathSync$1, copyFileSync, readdirSync as readdirSync$1, watch, rmdirSync, statSync as statSync$1 } from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import path__default, { join, delimiter, resolve as resolve$1, dirname as dirname$1, basename as basename$1 } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import { execFile,
|
|
8
|
+
import { execFile, execSync, spawn, exec as exec$1 } from 'child_process';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
|
-
import { randomUUID
|
|
11
|
-
import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
12
|
-
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, unlinkSync } from 'node:fs';
|
|
10
|
+
import { randomUUID, createHash } from 'crypto';
|
|
11
|
+
import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
|
|
13
13
|
import { exec, spawn as spawn$1, execSync as execSync$1, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import * as http from 'http';
|
|
16
16
|
import * as net from 'net';
|
|
17
|
-
import
|
|
17
|
+
import { createServer } from 'net';
|
|
18
|
+
import os, { homedir as homedir$1, platform as platform$1 } from 'node:os';
|
|
18
19
|
import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
|
|
19
20
|
import { EventEmitter } from 'node:events';
|
|
20
21
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
@@ -321,7 +322,26 @@ function buildSecurityContextFromFlags(flags) {
|
|
|
321
322
|
}
|
|
322
323
|
return ctx;
|
|
323
324
|
}
|
|
325
|
+
function unenforceableRules(method, context) {
|
|
326
|
+
const unsupported = [];
|
|
327
|
+
const fs = context.filesystem;
|
|
328
|
+
if (fs?.denyRead?.length) unsupported.push("filesystem.denyRead");
|
|
329
|
+
if (fs?.denyWrite?.length) unsupported.push("filesystem.denyWrite");
|
|
330
|
+
if (context.network?.deniedDomains?.length) unsupported.push("network.deniedDomains");
|
|
331
|
+
if (method === "docker" || method === "podman") {
|
|
332
|
+
if (fs?.allowWrite?.length) unsupported.push("filesystem.allowWrite");
|
|
333
|
+
if (context.network?.allowedDomains?.length) unsupported.push("network.allowedDomains");
|
|
334
|
+
}
|
|
335
|
+
return unsupported;
|
|
336
|
+
}
|
|
337
|
+
function describeUnenforceable(method, context) {
|
|
338
|
+
const unsupported = unenforceableRules(method, context);
|
|
339
|
+
if (!unsupported.length) return null;
|
|
340
|
+
return `securityContext: ${unsupported.join(", ")} is NOT ENFORCED by the "${method}" sandbox \u2014 these rules are accepted but have no effect (#0878). ` + (method === "nono" ? `nono is allow-list based and the workspace is granted via --allow-cwd, so a deny rule for a path inside the workspace does nothing. Use allowedDirectories / filesystem.allowWrite, or keep the sensitive path outside the workspace.` : `The container backend maps only allowedDirectories and denyAllNetwork. Use those, or run the machine under the nono sandbox.`);
|
|
341
|
+
}
|
|
324
342
|
function applySecurityContext(baseConfig, context) {
|
|
343
|
+
const warning = describeUnenforceable(baseConfig.method, context);
|
|
344
|
+
if (warning) console.warn(`[security] ${warning}`);
|
|
325
345
|
const config = { ...baseConfig };
|
|
326
346
|
if (config.method === "nono") {
|
|
327
347
|
const nonoConfig = { ...config.nonoConfig };
|
|
@@ -439,6 +459,7 @@ function buildSkillsPromptSection(skills) {
|
|
|
439
459
|
}
|
|
440
460
|
|
|
441
461
|
const READ_ONLY_TOOLS = ["get_context", "ask_session", "summarize_session", "use_skill"];
|
|
462
|
+
const MUTATING_TOOLS = ["run_bash", "send_to_session", "create_loop", "set_checklist", "stop_loop", "create_channel"];
|
|
442
463
|
const str$1 = (v) => v == null ? "" : String(v);
|
|
443
464
|
function buildTools(deps, skills) {
|
|
444
465
|
return [
|
|
@@ -1240,202 +1261,1223 @@ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
|
|
|
1240
1261
|
}
|
|
1241
1262
|
}
|
|
1242
1263
|
|
|
1243
|
-
|
|
1244
|
-
".
|
|
1245
|
-
".htm": "text/html; charset=utf-8",
|
|
1246
|
-
".js": "text/javascript; charset=utf-8",
|
|
1247
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
1248
|
-
".css": "text/css; charset=utf-8",
|
|
1249
|
-
".json": "application/json; charset=utf-8",
|
|
1250
|
-
".map": "application/json; charset=utf-8",
|
|
1251
|
-
".txt": "text/plain; charset=utf-8",
|
|
1252
|
-
".md": "text/plain; charset=utf-8",
|
|
1253
|
-
".xml": "application/xml; charset=utf-8",
|
|
1254
|
-
".csv": "text/csv; charset=utf-8",
|
|
1255
|
-
".svg": "image/svg+xml",
|
|
1256
|
-
".png": "image/png",
|
|
1257
|
-
".jpg": "image/jpeg",
|
|
1258
|
-
".jpeg": "image/jpeg",
|
|
1259
|
-
".gif": "image/gif",
|
|
1260
|
-
".webp": "image/webp",
|
|
1261
|
-
".avif": "image/avif",
|
|
1262
|
-
".ico": "image/x-icon",
|
|
1263
|
-
".bmp": "image/bmp",
|
|
1264
|
-
".mp4": "video/mp4",
|
|
1265
|
-
".webm": "video/webm",
|
|
1266
|
-
".mov": "video/quicktime",
|
|
1267
|
-
".m4v": "video/mp4",
|
|
1268
|
-
".mp3": "audio/mpeg",
|
|
1269
|
-
".wav": "audio/wav",
|
|
1270
|
-
".ogg": "audio/ogg",
|
|
1271
|
-
".m4a": "audio/mp4",
|
|
1272
|
-
".flac": "audio/flac",
|
|
1273
|
-
".pdf": "application/pdf",
|
|
1274
|
-
".wasm": "application/wasm",
|
|
1275
|
-
".zip": "application/zip",
|
|
1276
|
-
".gz": "application/gzip",
|
|
1277
|
-
".tar": "application/x-tar",
|
|
1278
|
-
".woff": "font/woff",
|
|
1279
|
-
".woff2": "font/woff2",
|
|
1280
|
-
".ttf": "font/ttf",
|
|
1281
|
-
".otf": "font/otf",
|
|
1282
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1283
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1284
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1285
|
-
};
|
|
1286
|
-
function contentTypeFor(filePath) {
|
|
1287
|
-
return MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
1264
|
+
function svampHome() {
|
|
1265
|
+
return process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1288
1266
|
}
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
for (const seg of decoded.split("/")) {
|
|
1292
|
-
if (!seg || seg === "." || seg === "..") continue;
|
|
1293
|
-
if (seg.startsWith(".") && seg !== ".well-known") return true;
|
|
1294
|
-
}
|
|
1295
|
-
return false;
|
|
1267
|
+
function cacheFile() {
|
|
1268
|
+
return join(svampHome(), "instance-config.json");
|
|
1296
1269
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
return
|
|
1270
|
+
const CONFIG_FILENAME = "svamp.json";
|
|
1271
|
+
let _config = null;
|
|
1272
|
+
let _loaded = false;
|
|
1273
|
+
let _cacheChecked = false;
|
|
1274
|
+
function stripSlash$1(u) {
|
|
1275
|
+
return u.replace(/\/+$/, "");
|
|
1303
1276
|
}
|
|
1304
|
-
function
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1277
|
+
function getInstanceUrl() {
|
|
1278
|
+
const raw = (process.env.SVAMP_INSTANCE_URL || "").trim();
|
|
1279
|
+
return raw ? stripSlash$1(raw) : void 0;
|
|
1280
|
+
}
|
|
1281
|
+
function configUrlsFor(anchor) {
|
|
1282
|
+
const base = stripSlash$1(anchor);
|
|
1283
|
+
return [`${base}/${CONFIG_FILENAME}`, `${base}/.well-known/${CONFIG_FILENAME}`];
|
|
1284
|
+
}
|
|
1285
|
+
async function fetchConfig(anchor) {
|
|
1286
|
+
for (const url of configUrlsFor(anchor)) {
|
|
1287
|
+
try {
|
|
1288
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
1289
|
+
if (!resp.ok) continue;
|
|
1290
|
+
const data = await resp.json();
|
|
1291
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
1292
|
+
return data;
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1312
1296
|
}
|
|
1297
|
+
return null;
|
|
1313
1298
|
}
|
|
1314
|
-
function
|
|
1299
|
+
function readCache() {
|
|
1315
1300
|
try {
|
|
1316
|
-
|
|
1301
|
+
if (existsSync$1(cacheFile())) {
|
|
1302
|
+
const data = JSON.parse(readFileSync$1(cacheFile(), "utf-8"));
|
|
1303
|
+
if (data && typeof data === "object" && !Array.isArray(data)) return data;
|
|
1304
|
+
}
|
|
1317
1305
|
} catch {
|
|
1318
|
-
return p;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
function parseRange(header, size) {
|
|
1322
|
-
if (!header) return null;
|
|
1323
|
-
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
1324
|
-
if (!m) return null;
|
|
1325
|
-
const [, s, e] = m;
|
|
1326
|
-
if (s === "" && e === "") return null;
|
|
1327
|
-
let start, end;
|
|
1328
|
-
if (s === "") {
|
|
1329
|
-
const n = parseInt(e, 10);
|
|
1330
|
-
if (n <= 0) return "unsatisfiable";
|
|
1331
|
-
start = Math.max(0, size - n);
|
|
1332
|
-
end = size - 1;
|
|
1333
|
-
} else {
|
|
1334
|
-
start = parseInt(s, 10);
|
|
1335
|
-
end = e === "" ? size - 1 : parseInt(e, 10);
|
|
1336
1306
|
}
|
|
1337
|
-
|
|
1338
|
-
if (end >= size) end = size - 1;
|
|
1339
|
-
return { start, end };
|
|
1340
|
-
}
|
|
1341
|
-
function directoryListingHtml(mountRelUrl, entries) {
|
|
1342
|
-
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1343
|
-
const base = mountRelUrl.endsWith("/") ? mountRelUrl : mountRelUrl + "/";
|
|
1344
|
-
const rows = entries.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1).map((e) => {
|
|
1345
|
-
const href = base + encodeURIComponent(e.name) + (e.isDir ? "/" : "");
|
|
1346
|
-
return `<li><a href="${esc(href)}">${esc(e.name)}${e.isDir ? "/" : ""}</a></li>`;
|
|
1347
|
-
}).join("\n");
|
|
1348
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(base)}</title>
|
|
1349
|
-
<style>body{font-family:system-ui,sans-serif;max-width:760px;margin:40px auto;padding:0 20px;color:#333}
|
|
1350
|
-
h1{font-size:1.15rem;word-break:break-all}ul{list-style:none;padding:0}li{padding:6px 0;border-bottom:1px solid #eee}
|
|
1351
|
-
a{color:#0969da;text-decoration:none}a:hover{text-decoration:underline}</style></head>
|
|
1352
|
-
<body><h1>${esc(base)}</h1><ul>${rows || "<li>(empty)</li>"}</ul></body></html>`;
|
|
1307
|
+
return null;
|
|
1353
1308
|
}
|
|
1354
|
-
|
|
1355
|
-
"Access-Control-Allow-Origin": "*",
|
|
1356
|
-
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
|
1357
|
-
"Access-Control-Allow-Headers": "*"
|
|
1358
|
-
};
|
|
1359
|
-
function serveStaticMount(req, res, opts) {
|
|
1360
|
-
const { rootDir, relPath, mountUrlPrefix } = opts;
|
|
1361
|
-
const browse = opts.browse !== false;
|
|
1362
|
-
const method = (req.method || "GET").toUpperCase();
|
|
1363
|
-
if (method === "OPTIONS") {
|
|
1364
|
-
res.writeHead(204, CORS);
|
|
1365
|
-
res.end();
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
1369
|
-
res.writeHead(405, { ...CORS, Allow: "GET, HEAD, OPTIONS" });
|
|
1370
|
-
res.end("Method Not Allowed");
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
let rootStat;
|
|
1309
|
+
function writeCache(cfg) {
|
|
1374
1310
|
try {
|
|
1375
|
-
|
|
1311
|
+
mkdirSync(svampHome(), { recursive: true });
|
|
1312
|
+
writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1376
1313
|
} catch {
|
|
1377
|
-
res.writeHead(404, CORS);
|
|
1378
|
-
res.end("Not Found");
|
|
1379
|
-
return;
|
|
1380
1314
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
res.writeHead(404, CORS);
|
|
1387
|
-
res.end("Not Found");
|
|
1388
|
-
return;
|
|
1315
|
+
}
|
|
1316
|
+
function applyConfig(cfg) {
|
|
1317
|
+
_config = cfg;
|
|
1318
|
+
if (cfg.hyphaServerUrl && typeof cfg.hyphaServerUrl === "string") {
|
|
1319
|
+
process.env.HYPHA_SERVER_URL = stripSlash$1(cfg.hyphaServerUrl.trim());
|
|
1389
1320
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1321
|
+
}
|
|
1322
|
+
async function loadInstanceConfig(opts = {}) {
|
|
1323
|
+
if (_loaded && !opts.force) return _config;
|
|
1324
|
+
const anchor = opts.anchor ? stripSlash$1(opts.anchor) : getInstanceUrl();
|
|
1325
|
+
if (anchor) {
|
|
1326
|
+
const fetched = await fetchConfig(anchor);
|
|
1327
|
+
if (fetched) {
|
|
1328
|
+
applyConfig(fetched);
|
|
1329
|
+
writeCache(fetched);
|
|
1330
|
+
_loaded = true;
|
|
1331
|
+
_cacheChecked = true;
|
|
1332
|
+
return _config;
|
|
1333
|
+
}
|
|
1395
1334
|
}
|
|
1396
|
-
const
|
|
1397
|
-
if (
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1335
|
+
const cached = readCache();
|
|
1336
|
+
if (cached) applyConfig(cached);
|
|
1337
|
+
_loaded = true;
|
|
1338
|
+
_cacheChecked = true;
|
|
1339
|
+
return _config;
|
|
1340
|
+
}
|
|
1341
|
+
function getLoadedConfig() {
|
|
1342
|
+
if (!_loaded && !_cacheChecked) {
|
|
1343
|
+
const cached = readCache();
|
|
1344
|
+
if (cached) applyConfig(cached);
|
|
1345
|
+
_cacheChecked = true;
|
|
1401
1346
|
}
|
|
1402
|
-
|
|
1347
|
+
return _config;
|
|
1348
|
+
}
|
|
1349
|
+
function clearInstanceConfigCache() {
|
|
1350
|
+
_config = null;
|
|
1351
|
+
_loaded = false;
|
|
1352
|
+
_cacheChecked = false;
|
|
1403
1353
|
try {
|
|
1404
|
-
|
|
1354
|
+
if (existsSync$1(cacheFile())) rmSync(cacheFile());
|
|
1405
1355
|
} catch {
|
|
1406
|
-
res.writeHead(404, { ...CORS, "Content-Type": "text/plain" });
|
|
1407
|
-
res.end("Not Found");
|
|
1408
|
-
return;
|
|
1409
1356
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1357
|
+
}
|
|
1358
|
+
function __setConfigForTest(cfg) {
|
|
1359
|
+
_config = cfg;
|
|
1360
|
+
_loaded = true;
|
|
1361
|
+
_cacheChecked = true;
|
|
1362
|
+
if (cfg) applyConfig(cfg);
|
|
1363
|
+
}
|
|
1364
|
+
function __resetForTest() {
|
|
1365
|
+
_config = null;
|
|
1366
|
+
_loaded = false;
|
|
1367
|
+
_cacheChecked = false;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
var instanceConfig = /*#__PURE__*/Object.freeze({
|
|
1371
|
+
__proto__: null,
|
|
1372
|
+
CONFIG_FILENAME: CONFIG_FILENAME,
|
|
1373
|
+
__resetForTest: __resetForTest,
|
|
1374
|
+
__setConfigForTest: __setConfigForTest,
|
|
1375
|
+
clearInstanceConfigCache: clearInstanceConfigCache,
|
|
1376
|
+
configUrlsFor: configUrlsFor,
|
|
1377
|
+
getInstanceUrl: getInstanceUrl,
|
|
1378
|
+
getLoadedConfig: getLoadedConfig,
|
|
1379
|
+
loadInstanceConfig: loadInstanceConfig
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
function stripSlash(u) {
|
|
1383
|
+
return u.replace(/\/+$/, "");
|
|
1384
|
+
}
|
|
1385
|
+
function getHyphaServerUrl() {
|
|
1386
|
+
const fromCfg = getLoadedConfig()?.hyphaServerUrl;
|
|
1387
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
1388
|
+
const env = (process.env.HYPHA_SERVER_URL || "").trim();
|
|
1389
|
+
return env ? stripSlash(env) : void 0;
|
|
1390
|
+
}
|
|
1391
|
+
function getFrpsSubdomainHost() {
|
|
1392
|
+
const fromCfg = getLoadedConfig()?.frps?.subdomainHost;
|
|
1393
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
1394
|
+
const env = (process.env.FRPS_SUBDOMAIN_HOST || "").trim();
|
|
1395
|
+
if (env) return env;
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
'frps subdomain host is not configured. Serve a svamp.json with "frps.subdomainHost" at your frontend URL (svamp login <frontend-url>), or set FRPS_SUBDOMAIN_HOST (legacy). It is deployment-specific and never guessed.'
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
function getFrpsServerAddr() {
|
|
1401
|
+
const fromCfg = getLoadedConfig()?.frps?.serverAddr;
|
|
1402
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
1403
|
+
const env = (process.env.FRPS_SERVER_ADDR || "").trim();
|
|
1404
|
+
if (env) return env;
|
|
1405
|
+
return `frps-ctrl.${getFrpsSubdomainHost()}`;
|
|
1406
|
+
}
|
|
1407
|
+
function getFrpsServerPort() {
|
|
1408
|
+
const fromCfg = getLoadedConfig()?.frps?.serverPort;
|
|
1409
|
+
if (typeof fromCfg === "number" && Number.isFinite(fromCfg)) return fromCfg;
|
|
1410
|
+
const env = parseInt(process.env.FRPS_SERVER_PORT || "", 10);
|
|
1411
|
+
return Number.isFinite(env) ? env : void 0;
|
|
1412
|
+
}
|
|
1413
|
+
function getSvampWebBaseUrl() {
|
|
1414
|
+
const fromCfg = getLoadedConfig()?.web?.baseUrl;
|
|
1415
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
1416
|
+
const env = (process.env.SVAMP_WEB_URL || "").trim();
|
|
1417
|
+
if (env) return stripSlash(env);
|
|
1418
|
+
const instance = getInstanceUrl();
|
|
1419
|
+
if (instance) return instance;
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
'Svamp web base URL is not configured. Serve a svamp.json with "web.baseUrl", log in with your frontend URL (svamp login <frontend-url>), or set SVAMP_WEB_URL (legacy).'
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
function getSkillsServerUrl() {
|
|
1425
|
+
const fromCfg = getLoadedConfig()?.skills?.server;
|
|
1426
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
1427
|
+
const env = (process.env.HYPHA_SKILLS_SERVER || "").trim();
|
|
1428
|
+
if (env) return stripSlash(env);
|
|
1429
|
+
const server = getHyphaServerUrl();
|
|
1430
|
+
if (server) return server;
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
"Cannot determine skills marketplace server. Serve a svamp.json or set HYPHA_SERVER_URL."
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
function getSkillsWorkspace() {
|
|
1436
|
+
const fromCfg = getLoadedConfig()?.skills?.workspace;
|
|
1437
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
1438
|
+
return (process.env.HYPHA_SKILLS_WORKSPACE || "").trim() || "hypha-cloud";
|
|
1439
|
+
}
|
|
1440
|
+
function getSkillsCollection() {
|
|
1441
|
+
const fromCfg = getLoadedConfig()?.skills?.collection;
|
|
1442
|
+
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
1443
|
+
return (process.env.HYPHA_SKILLS_COLLECTION || "").trim() || "marketplace";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function getMachineFingerprint() {
|
|
1447
|
+
const idFile = join(homedir(), ".svamp", "machine-id");
|
|
1448
|
+
try {
|
|
1449
|
+
const existing = readFileSync$1(idFile, "utf-8").trim();
|
|
1450
|
+
if (existing) return existing;
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
const id = randomUUID();
|
|
1454
|
+
try {
|
|
1455
|
+
mkdirSync(join(homedir(), ".svamp"), { recursive: true });
|
|
1456
|
+
writeFileSync(idFile, id + "\n");
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
return id;
|
|
1460
|
+
}
|
|
1461
|
+
const FRP_VERSION = "0.68.0";
|
|
1462
|
+
const BIN_DIR = join(homedir(), ".svamp", "bin");
|
|
1463
|
+
const FRPC_BIN = join(BIN_DIR, platform() === "win32" ? "frpc.exe" : "frpc");
|
|
1464
|
+
const _livingFrpcPids = /* @__PURE__ */ new Set();
|
|
1465
|
+
function isFrpcSubdomainConflict(errorLine) {
|
|
1466
|
+
return /router config conflict|already exists/i.test(errorLine);
|
|
1467
|
+
}
|
|
1468
|
+
function shouldGiveUpOnConflict(args) {
|
|
1469
|
+
if (args.everStartedProxy) return false;
|
|
1470
|
+
return args.conflictCount >= args.minAttempts && args.elapsedMs >= args.minElapsedMs;
|
|
1471
|
+
}
|
|
1472
|
+
function selectOrphanedFrpcPids(psOutput, selfPid, livingPids) {
|
|
1473
|
+
const pids = [];
|
|
1474
|
+
for (const line of psOutput.split("\n")) {
|
|
1475
|
+
const m = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
1476
|
+
if (!m) continue;
|
|
1477
|
+
const pid = Number(m[1]);
|
|
1478
|
+
const cmd = m[2];
|
|
1479
|
+
if (!cmd.includes(".svamp/bin/frpc")) continue;
|
|
1480
|
+
if (pid === selfPid || livingPids.has(pid)) continue;
|
|
1481
|
+
pids.push(pid);
|
|
1482
|
+
}
|
|
1483
|
+
return pids;
|
|
1484
|
+
}
|
|
1485
|
+
function killOrphanedFrpc(log) {
|
|
1486
|
+
if (process.platform === "win32") return;
|
|
1487
|
+
try {
|
|
1488
|
+
const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
|
|
1489
|
+
const pids = selectOrphanedFrpcPids(out, process.pid, _livingFrpcPids);
|
|
1490
|
+
for (const pid of pids) {
|
|
1491
|
+
try {
|
|
1492
|
+
process.kill(pid, "SIGKILL");
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (pids.length) {
|
|
1497
|
+
log(`Reaped ${pids.length} orphaned frpc process(es) from a prior daemon generation \u2014 they were retrying stale subdomains and causing frps router-config-conflict churn.`);
|
|
1498
|
+
}
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function isInCluster() {
|
|
1503
|
+
return !!(process.env.KUBERNETES_SERVICE_HOST || process.env.SANDBOX_ID);
|
|
1504
|
+
}
|
|
1505
|
+
const FRPS_CLUSTER_ADDR = "frps.hypha.svc.cluster.local";
|
|
1506
|
+
const DEFAULT_FRPS_PORT = isInCluster() ? 7e3 : 443;
|
|
1507
|
+
const FRPS_PUBLIC_TOKEN = "hypha-frps-public";
|
|
1508
|
+
function getFrpcDownloadUrl() {
|
|
1509
|
+
const os = platform();
|
|
1510
|
+
const a = arch();
|
|
1511
|
+
let osStr;
|
|
1512
|
+
let archStr;
|
|
1513
|
+
switch (os) {
|
|
1514
|
+
case "darwin":
|
|
1515
|
+
osStr = "darwin";
|
|
1516
|
+
break;
|
|
1517
|
+
case "linux":
|
|
1518
|
+
osStr = "linux";
|
|
1519
|
+
break;
|
|
1520
|
+
case "win32":
|
|
1521
|
+
osStr = "windows";
|
|
1522
|
+
break;
|
|
1523
|
+
default:
|
|
1524
|
+
throw new Error(`Unsupported platform: ${os}`);
|
|
1525
|
+
}
|
|
1526
|
+
switch (a) {
|
|
1527
|
+
case "x64":
|
|
1528
|
+
archStr = "amd64";
|
|
1529
|
+
break;
|
|
1530
|
+
case "arm64":
|
|
1531
|
+
archStr = "arm64";
|
|
1532
|
+
break;
|
|
1533
|
+
case "arm":
|
|
1534
|
+
archStr = "arm";
|
|
1535
|
+
break;
|
|
1536
|
+
default:
|
|
1537
|
+
throw new Error(`Unsupported architecture: ${a}`);
|
|
1538
|
+
}
|
|
1539
|
+
const ext = os === "win32" ? "zip" : "tar.gz";
|
|
1540
|
+
const filename = `frp_${FRP_VERSION}_${osStr}_${archStr}`;
|
|
1541
|
+
return `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${filename}.${ext}`;
|
|
1542
|
+
}
|
|
1543
|
+
let _frpcDownloadInFlight = null;
|
|
1544
|
+
async function ensureFrpc(log) {
|
|
1545
|
+
if (existsSync$1(FRPC_BIN)) {
|
|
1546
|
+
try {
|
|
1547
|
+
const out = execSync(`"${FRPC_BIN}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
|
|
1548
|
+
if (out === FRP_VERSION) return FRPC_BIN;
|
|
1549
|
+
(log || console.log)(`frpc version mismatch: ${out} \u2260 ${FRP_VERSION}, re-downloading...`);
|
|
1550
|
+
} catch {
|
|
1551
|
+
(log || console.log)("frpc binary broken or invalid signature, re-downloading...");
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (_frpcDownloadInFlight) return _frpcDownloadInFlight;
|
|
1555
|
+
_frpcDownloadInFlight = downloadFrpc(log).finally(() => {
|
|
1556
|
+
_frpcDownloadInFlight = null;
|
|
1557
|
+
});
|
|
1558
|
+
return _frpcDownloadInFlight;
|
|
1559
|
+
}
|
|
1560
|
+
async function downloadFrpc(log) {
|
|
1561
|
+
try {
|
|
1562
|
+
if (existsSync$1(FRPC_BIN)) unlinkSync(FRPC_BIN);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
const logger = log || console.log;
|
|
1566
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
1567
|
+
const url = getFrpcDownloadUrl();
|
|
1568
|
+
logger(`Downloading frpc ${FRP_VERSION} from ${url}...`);
|
|
1569
|
+
const ctrl = new AbortController();
|
|
1570
|
+
const timer = setTimeout(() => ctrl.abort(), 6e4);
|
|
1571
|
+
let response;
|
|
1572
|
+
try {
|
|
1573
|
+
response = await fetch(url, { signal: ctrl.signal });
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
const why = err?.name === "AbortError" ? "timed out after 60s" : err?.message ?? String(err);
|
|
1576
|
+
throw new Error(`Failed to download frpc from ${url}: ${why}`);
|
|
1577
|
+
} finally {
|
|
1578
|
+
clearTimeout(timer);
|
|
1579
|
+
}
|
|
1580
|
+
if (!response.ok) {
|
|
1581
|
+
throw new Error(`Failed to download frpc: ${response.status} ${response.statusText}`);
|
|
1582
|
+
}
|
|
1583
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1584
|
+
if (platform() === "win32") {
|
|
1585
|
+
throw new Error("Windows ZIP extraction not implemented \u2014 please install frpc manually");
|
|
1586
|
+
}
|
|
1587
|
+
const tmpTar = join(BIN_DIR, `frpc-${FRP_VERSION}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tar.gz`);
|
|
1588
|
+
writeFileSync(tmpTar, buffer);
|
|
1589
|
+
try {
|
|
1590
|
+
const dirName = `frp_${FRP_VERSION}_${platform() === "darwin" ? "darwin" : "linux"}_${arch() === "x64" ? "amd64" : arch()}`;
|
|
1591
|
+
execSync(
|
|
1592
|
+
`tar -xzf "${tmpTar}" -C "${BIN_DIR}" --strip-components=1 "${dirName}/frpc"`,
|
|
1593
|
+
{ stdio: "pipe" }
|
|
1594
|
+
);
|
|
1595
|
+
chmodSync(FRPC_BIN, 493);
|
|
1596
|
+
logger(`frpc installed at ${FRPC_BIN}`);
|
|
1597
|
+
} finally {
|
|
1598
|
+
try {
|
|
1599
|
+
unlinkSync(tmpTar);
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return FRPC_BIN;
|
|
1604
|
+
}
|
|
1605
|
+
function tomlStr(v) {
|
|
1606
|
+
return String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1607
|
+
}
|
|
1608
|
+
function generateFrpcConfig(config, proxies, admin) {
|
|
1609
|
+
const useWSS = config.serverPort === 443;
|
|
1610
|
+
const lines = [
|
|
1611
|
+
"# Auto-generated by svamp \u2014 do not edit",
|
|
1612
|
+
`serverAddr = "${tomlStr(config.serverAddr)}"`,
|
|
1613
|
+
`serverPort = ${config.serverPort}`,
|
|
1614
|
+
"",
|
|
1615
|
+
'auth.method = "token"',
|
|
1616
|
+
`auth.token = "${tomlStr(config.authToken)}"`,
|
|
1617
|
+
"",
|
|
1618
|
+
"# Hypha JWT token for server-side authentication",
|
|
1619
|
+
...config.hyphaToken ? [`metadatas.token = "${tomlStr(config.hyphaToken)}"`] : [],
|
|
1620
|
+
"",
|
|
1621
|
+
"# Transport",
|
|
1622
|
+
...useWSS ? ['transport.protocol = "wss"'] : [],
|
|
1623
|
+
// 15s/45s heartbeat — 2x faster failure detection than the frpc default
|
|
1624
|
+
// (30s/90s). With matching server-side frps 0.68.x, dead-peer detection
|
|
1625
|
+
// settles in well under a minute, so the daemon-side recreate threshold
|
|
1626
|
+
// can be tightened too.
|
|
1627
|
+
"transport.heartbeatInterval = 15",
|
|
1628
|
+
"transport.heartbeatTimeout = 45",
|
|
1629
|
+
"transport.poolCount = 5",
|
|
1630
|
+
"",
|
|
1631
|
+
"# Don't exit on login failure \u2014 let frpc keep retrying",
|
|
1632
|
+
"loginFailExit = false",
|
|
1633
|
+
"",
|
|
1634
|
+
'log.to = "console"',
|
|
1635
|
+
'log.level = "info"',
|
|
1636
|
+
""
|
|
1637
|
+
];
|
|
1638
|
+
if (admin) {
|
|
1639
|
+
lines.push("# Local admin/status API (loopback only \u2014 polled by the svamp daemon)");
|
|
1640
|
+
lines.push('webServer.addr = "127.0.0.1"');
|
|
1641
|
+
lines.push(`webServer.port = ${admin.port}`);
|
|
1642
|
+
lines.push(`webServer.user = "${tomlStr(admin.user)}"`);
|
|
1643
|
+
lines.push(`webServer.password = "${tomlStr(admin.password)}"`);
|
|
1644
|
+
lines.push("");
|
|
1645
|
+
}
|
|
1646
|
+
for (const proxy of proxies) {
|
|
1647
|
+
lines.push(`[[proxies]]`);
|
|
1648
|
+
lines.push(`name = "${tomlStr(proxy.name)}"`);
|
|
1649
|
+
lines.push(`type = "${tomlStr(proxy.type)}"`);
|
|
1650
|
+
lines.push(`localIP = "${tomlStr(proxy.localIP || "127.0.0.1")}"`);
|
|
1651
|
+
lines.push(`localPort = ${proxy.localPort}`);
|
|
1652
|
+
if (proxy.subdomain) {
|
|
1653
|
+
lines.push(`subdomain = "${tomlStr(proxy.subdomain)}"`);
|
|
1654
|
+
}
|
|
1655
|
+
if (proxy.customDomains && proxy.customDomains.length > 0) {
|
|
1656
|
+
lines.push(`customDomains = [${proxy.customDomains.map((d) => `"${tomlStr(d)}"`).join(", ")}]`);
|
|
1657
|
+
}
|
|
1658
|
+
if (proxy.group) {
|
|
1659
|
+
lines.push(`loadBalancer.group = "${tomlStr(proxy.group)}"`);
|
|
1660
|
+
if (proxy.groupKey) {
|
|
1661
|
+
lines.push(`loadBalancer.groupKey = "${tomlStr(proxy.groupKey)}"`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (proxy.healthCheckType) {
|
|
1665
|
+
lines.push(`healthCheck.type = "${tomlStr(proxy.healthCheckType)}"`);
|
|
1666
|
+
if (proxy.healthCheckType === "http" && proxy.healthCheckPath) {
|
|
1667
|
+
lines.push(`healthCheck.path = "${tomlStr(proxy.healthCheckPath)}"`);
|
|
1668
|
+
}
|
|
1669
|
+
lines.push(`healthCheck.intervalSeconds = ${proxy.healthCheckInterval || 10}`);
|
|
1670
|
+
lines.push(`healthCheck.timeoutSeconds = ${proxy.healthCheckTimeout || 3}`);
|
|
1671
|
+
lines.push(`healthCheck.maxFailed = ${proxy.healthCheckMaxFailed || 3}`);
|
|
1672
|
+
}
|
|
1673
|
+
if (proxy.type === "http") {
|
|
1674
|
+
lines.push("transport.useEncryption = false");
|
|
1675
|
+
lines.push("transport.useCompression = false");
|
|
1676
|
+
}
|
|
1677
|
+
lines.push("");
|
|
1678
|
+
}
|
|
1679
|
+
return lines.join("\n");
|
|
1680
|
+
}
|
|
1681
|
+
function getFreePort() {
|
|
1682
|
+
return new Promise((resolve, reject) => {
|
|
1683
|
+
const srv = createServer();
|
|
1684
|
+
srv.unref();
|
|
1685
|
+
srv.on("error", reject);
|
|
1686
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
1687
|
+
const addr = srv.address();
|
|
1688
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1689
|
+
srv.close(() => port ? resolve(port) : reject(new Error("no port")));
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
function parseFrpcStatus(payload, proxyNames) {
|
|
1694
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1695
|
+
if (payload && typeof payload === "object") {
|
|
1696
|
+
for (const group of Object.values(payload)) {
|
|
1697
|
+
if (!Array.isArray(group)) continue;
|
|
1698
|
+
for (const p of group) {
|
|
1699
|
+
if (p && typeof p.name === "string") {
|
|
1700
|
+
byName.set(p.name, { status: String(p.status ?? ""), err: p.err ? String(p.err) : void 0 });
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const missing = [];
|
|
1706
|
+
const failed = [];
|
|
1707
|
+
for (const name of proxyNames) {
|
|
1708
|
+
const entry = byName.get(name);
|
|
1709
|
+
if (!entry) {
|
|
1710
|
+
missing.push(name);
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
if (entry.status !== "running") failed.push({ name, status: entry.status, err: entry.err });
|
|
1714
|
+
}
|
|
1715
|
+
return { allRunning: missing.length === 0 && failed.length === 0, missing, failed };
|
|
1716
|
+
}
|
|
1717
|
+
class FrpcTunnel {
|
|
1718
|
+
process = null;
|
|
1719
|
+
_connected = false;
|
|
1720
|
+
_destroyed = false;
|
|
1721
|
+
// #0460: in-flight guard. `this.process` is only set AFTER the `await ensureFrpc()`
|
|
1722
|
+
// inside connect(), so two connect() calls interleaved across that await (e.g. the
|
|
1723
|
+
// auto-restart timer firing while an external caller also connects) both pass the
|
|
1724
|
+
// `if (this.process) return` null-check and spawn a duplicate frpc child — the first
|
|
1725
|
+
// is overwritten and never killed, leaving a ghost tunnel registration. This flag is
|
|
1726
|
+
// set synchronously before the await and makes connect() idempotent across it.
|
|
1727
|
+
_connecting = false;
|
|
1728
|
+
configPath;
|
|
1729
|
+
options;
|
|
1730
|
+
serverConfig;
|
|
1731
|
+
log;
|
|
1732
|
+
logBuffer = [];
|
|
1733
|
+
maxLogLines = 200;
|
|
1734
|
+
proxies = [];
|
|
1735
|
+
// Health tracking
|
|
1736
|
+
_consecutiveErrors = 0;
|
|
1737
|
+
_lastConnectedAt = 0;
|
|
1738
|
+
_lastErrorAt = 0;
|
|
1739
|
+
_firstErrorAt = 0;
|
|
1740
|
+
_restartAttempts = 0;
|
|
1741
|
+
// Persistent-conflict give-up (kth-k8s nightly review 2026-08-09). A generation that has NEVER
|
|
1742
|
+
// brought a proxy online and only ever gets `router config conflict` / `already exists` has lost
|
|
1743
|
+
// the vhost race to another live generation — it must STOP hammering frps ~2/min forever rather
|
|
1744
|
+
// than retry indefinitely. Reset on any real `start proxy success`; only fires when a proxy was
|
|
1745
|
+
// never started (so a live tunnel riding out an frps failover is never torn down).
|
|
1746
|
+
_everStartedProxy = false;
|
|
1747
|
+
_conflictCount = 0;
|
|
1748
|
+
_conflictSince = 0;
|
|
1749
|
+
_restartTimer = null;
|
|
1750
|
+
// #0583: pending reconnect timer (cancelled on destroy)
|
|
1751
|
+
// End-to-end probe state (set when options.probeUrl is provided).
|
|
1752
|
+
_probeTimer = null;
|
|
1753
|
+
_burstTimer = null;
|
|
1754
|
+
// #0622: probe burst timer (cancelled on stopProbeLoop/destroy)
|
|
1755
|
+
_connectTimeoutTimer = null;
|
|
1756
|
+
// #0622: connect() 30s fallback (unref'd + cleared)
|
|
1757
|
+
_lastProbeOkAt = 0;
|
|
1758
|
+
_lastProbeFailAt = 0;
|
|
1759
|
+
_probeOk = false;
|
|
1760
|
+
// frpc admin/status API state (set when options.adminStatus is true).
|
|
1761
|
+
_adminPort = 0;
|
|
1762
|
+
_adminUser = "svamp";
|
|
1763
|
+
_adminPassword = randomUUID();
|
|
1764
|
+
_statusTimer = null;
|
|
1765
|
+
constructor(options) {
|
|
1766
|
+
this.options = options;
|
|
1767
|
+
this.log = options.log || ((msg) => console.log(`[FRPC] ${msg}`));
|
|
1768
|
+
const hyphaToken = options.serverConfig?.hyphaToken || process.env.HYPHA_TOKEN || "";
|
|
1769
|
+
this.serverConfig = {
|
|
1770
|
+
serverAddr: options.serverConfig?.serverAddr || process.env.FRPS_SERVER_ADDR || (isInCluster() ? FRPS_CLUSTER_ADDR : getFrpsServerAddr()),
|
|
1771
|
+
serverPort: options.serverConfig?.serverPort || getFrpsServerPort() || DEFAULT_FRPS_PORT,
|
|
1772
|
+
authToken: options.serverConfig?.authToken || process.env.FRPS_AUTH_TOKEN || FRPS_PUBLIC_TOKEN,
|
|
1773
|
+
hyphaToken,
|
|
1774
|
+
subDomainHost: options.serverConfig?.subDomainHost || process.env.FRPS_SUBDOMAIN_HOST || getFrpsSubdomainHost()
|
|
1775
|
+
};
|
|
1776
|
+
if (!this.serverConfig.hyphaToken) {
|
|
1777
|
+
throw new Error(
|
|
1778
|
+
'Hypha token required for frpc authentication. Run "svamp login" or set HYPHA_TOKEN.'
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
const machineFingerprint = getMachineFingerprint();
|
|
1782
|
+
const machineHash = createHash("sha256").update(machineFingerprint).digest("hex").slice(0, 6);
|
|
1783
|
+
const multiPort = options.ports.length > 1;
|
|
1784
|
+
this.proxies = options.ports.map((port) => {
|
|
1785
|
+
const standaloneHash = createHash("sha256").update(multiPort ? `${options.name}|${port}|${machineFingerprint}` : `${options.name}|${machineFingerprint}`).digest("hex").slice(0, 8);
|
|
1786
|
+
const groupSubdomainKey = multiPort ? `${options.group}|${port}` : `${options.group}`;
|
|
1787
|
+
const subdomain = options.subdomains?.get(port) || (options.group ? `${options.group}-${createHash("sha256").update(groupSubdomainKey).digest("hex").slice(0, 8)}` : `${options.name}-${standaloneHash}`);
|
|
1788
|
+
const proxyName = `${options.name}-${port}-${machineHash}`;
|
|
1789
|
+
return {
|
|
1790
|
+
name: proxyName,
|
|
1791
|
+
type: "http",
|
|
1792
|
+
localIP: options.localHost || "127.0.0.1",
|
|
1793
|
+
localPort: port,
|
|
1794
|
+
subdomain,
|
|
1795
|
+
group: options.group,
|
|
1796
|
+
groupKey: options.groupKey,
|
|
1797
|
+
healthCheckType: options.healthCheckType,
|
|
1798
|
+
healthCheckPath: options.healthCheckPath,
|
|
1799
|
+
healthCheckInterval: options.healthCheckInterval
|
|
1800
|
+
};
|
|
1801
|
+
});
|
|
1802
|
+
const configDir = join(homedir(), ".svamp", "frpc");
|
|
1803
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
1804
|
+
try {
|
|
1805
|
+
chmodSync(configDir, 448);
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
this.configPath = join(configDir, `${options.name}.toml`);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* #0583: schedule a backoff reconnect after the child went down (spawn error OR exit).
|
|
1812
|
+
* Extracted from the exit handler so BOTH the spawn-'error' and 'exit' paths share it, and
|
|
1813
|
+
* so it is unit-testable without a real spawn. No-op once destroyed.
|
|
1814
|
+
*/
|
|
1815
|
+
_reconnectAfterDown(reason) {
|
|
1816
|
+
if (this._destroyed) return;
|
|
1817
|
+
const STABILITY_WINDOW_MS = 3e4;
|
|
1818
|
+
if (this._lastConnectedAt > 0 && Date.now() - this._lastConnectedAt >= STABILITY_WINDOW_MS) {
|
|
1819
|
+
this._restartAttempts = 0;
|
|
1820
|
+
}
|
|
1821
|
+
this._restartAttempts++;
|
|
1822
|
+
const delay = Math.min(3e3 * Math.pow(2, this._restartAttempts - 1), 6e4);
|
|
1823
|
+
this.log(`frpc down (${reason}), restarting in ${Math.round(delay / 1e3)}s (attempt ${this._restartAttempts})...`);
|
|
1824
|
+
this._restartTimer = setTimeout(() => {
|
|
1825
|
+
this._restartTimer = null;
|
|
1826
|
+
if (!this._destroyed) {
|
|
1827
|
+
this.connect().catch((err) => {
|
|
1828
|
+
this.options.onError?.(err);
|
|
1829
|
+
if (!this._destroyed && !this._restartTimer && !this.process) {
|
|
1830
|
+
this._reconnectAfterDown(`reconnect setup failed: ${err?.message ?? err}`);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}, delay);
|
|
1835
|
+
}
|
|
1836
|
+
/** Connect to frps by starting the frpc process. */
|
|
1837
|
+
async connect() {
|
|
1838
|
+
if (this._destroyed) return;
|
|
1839
|
+
if (this.process) return;
|
|
1840
|
+
if (this._connecting) return;
|
|
1841
|
+
this._connecting = true;
|
|
1842
|
+
let frpcPath;
|
|
1843
|
+
let admin;
|
|
1844
|
+
try {
|
|
1845
|
+
frpcPath = await ensureFrpc(this.log);
|
|
1846
|
+
if (this.options.adminStatus) {
|
|
1847
|
+
if (!this._adminPort) {
|
|
1848
|
+
try {
|
|
1849
|
+
this._adminPort = await getFreePort();
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
this.log(`admin port alloc failed: ${err?.message ?? err}`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (this._adminPort) admin = { port: this._adminPort, user: this._adminUser, password: this._adminPassword };
|
|
1855
|
+
}
|
|
1856
|
+
const configContent = generateFrpcConfig(this.serverConfig, this.proxies, admin);
|
|
1857
|
+
writeFileSync(this.configPath, configContent, { mode: 384 });
|
|
1858
|
+
this.log(`Config written to ${this.configPath}`);
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
this._connecting = false;
|
|
1861
|
+
throw err;
|
|
1862
|
+
}
|
|
1863
|
+
if (this._destroyed) {
|
|
1864
|
+
this._connecting = false;
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
return new Promise((resolve, reject) => {
|
|
1868
|
+
const child = spawn(frpcPath, ["-c", this.configPath], {
|
|
1869
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1870
|
+
env: { ...process.env }
|
|
1871
|
+
});
|
|
1872
|
+
this.process = child;
|
|
1873
|
+
if (child.pid) _livingFrpcPids.add(child.pid);
|
|
1874
|
+
this._connecting = false;
|
|
1875
|
+
let resolved = false;
|
|
1876
|
+
const onOutput = (data) => {
|
|
1877
|
+
const text = data.toString().trim();
|
|
1878
|
+
if (!text) return;
|
|
1879
|
+
for (const line of text.split("\n")) {
|
|
1880
|
+
this.logBuffer.push(line);
|
|
1881
|
+
if (this.logBuffer.length > this.maxLogLines) {
|
|
1882
|
+
this.logBuffer.shift();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (text.includes("start proxy success")) {
|
|
1886
|
+
this._everStartedProxy = true;
|
|
1887
|
+
this._conflictCount = 0;
|
|
1888
|
+
this._conflictSince = 0;
|
|
1889
|
+
}
|
|
1890
|
+
if (text.includes("start proxy success") || text.includes("login to server success")) {
|
|
1891
|
+
if (!this._connected) {
|
|
1892
|
+
this._connected = true;
|
|
1893
|
+
this._lastConnectedAt = Date.now();
|
|
1894
|
+
this._consecutiveErrors = 0;
|
|
1895
|
+
this._firstErrorAt = 0;
|
|
1896
|
+
this.options.onConnect?.();
|
|
1897
|
+
const probeConfigured = !!(this.options.probeUrl || this.options.probePath);
|
|
1898
|
+
if (!resolved && !probeConfigured) {
|
|
1899
|
+
resolved = true;
|
|
1900
|
+
resolve();
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const errorLine = text.split("\n").find((line) => line.includes("[E]"));
|
|
1905
|
+
if (errorLine) {
|
|
1906
|
+
this._consecutiveErrors++;
|
|
1907
|
+
this._lastErrorAt = Date.now();
|
|
1908
|
+
if (this._firstErrorAt === 0) this._firstErrorAt = Date.now();
|
|
1909
|
+
this.options.onError?.(new Error(errorLine));
|
|
1910
|
+
if (isFrpcSubdomainConflict(errorLine) && !this._everStartedProxy) {
|
|
1911
|
+
if (this._conflictSince === 0) this._conflictSince = Date.now();
|
|
1912
|
+
this._conflictCount++;
|
|
1913
|
+
const CONFLICT_GIVEUP_ATTEMPTS = Number(process.env.SVAMP_FRPC_CONFLICT_GIVEUP_ATTEMPTS || 6);
|
|
1914
|
+
const CONFLICT_GIVEUP_MS = Number(process.env.SVAMP_FRPC_CONFLICT_GIVEUP_MS || 6e4);
|
|
1915
|
+
const elapsed = Date.now() - this._conflictSince;
|
|
1916
|
+
if (shouldGiveUpOnConflict({
|
|
1917
|
+
everStartedProxy: this._everStartedProxy,
|
|
1918
|
+
conflictCount: this._conflictCount,
|
|
1919
|
+
elapsedMs: elapsed,
|
|
1920
|
+
minAttempts: CONFLICT_GIVEUP_ATTEMPTS,
|
|
1921
|
+
minElapsedMs: CONFLICT_GIVEUP_MS
|
|
1922
|
+
})) {
|
|
1923
|
+
this.log(
|
|
1924
|
+
`Giving up: ${this._conflictCount} subdomain conflicts over ${Math.round(elapsed / 1e3)}s with no proxy ever online \u2014 another generation owns this vhost. Stopping to avoid endless frps churn.`
|
|
1925
|
+
);
|
|
1926
|
+
this.options.onError?.(new Error(`frpc persistent subdomain conflict (${this._conflictCount}x) \u2014 giving up`));
|
|
1927
|
+
this.destroy();
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
child.stdout?.on("data", onOutput);
|
|
1933
|
+
child.stderr?.on("data", onOutput);
|
|
1934
|
+
let downHandled = false;
|
|
1935
|
+
const onChildDown = (rejectErr) => {
|
|
1936
|
+
if (downHandled) return;
|
|
1937
|
+
downHandled = true;
|
|
1938
|
+
this._connected = false;
|
|
1939
|
+
if (child.pid) _livingFrpcPids.delete(child.pid);
|
|
1940
|
+
this.process = null;
|
|
1941
|
+
this.options.onDisconnect?.();
|
|
1942
|
+
if (!resolved) {
|
|
1943
|
+
resolved = true;
|
|
1944
|
+
reject(rejectErr);
|
|
1945
|
+
}
|
|
1946
|
+
this._reconnectAfterDown(rejectErr.message);
|
|
1947
|
+
};
|
|
1948
|
+
child.on("error", (err) => {
|
|
1949
|
+
this.options.onError?.(err);
|
|
1950
|
+
onChildDown(err instanceof Error ? err : new Error(String(err)));
|
|
1951
|
+
});
|
|
1952
|
+
child.on("exit", (code) => {
|
|
1953
|
+
onChildDown(new Error(`frpc exited with code ${code}`));
|
|
1954
|
+
});
|
|
1955
|
+
if (this.options.probeUrl || this.options.probePath) {
|
|
1956
|
+
this.startProbeLoop({
|
|
1957
|
+
onFirstOk: () => {
|
|
1958
|
+
if (!resolved) {
|
|
1959
|
+
resolved = true;
|
|
1960
|
+
resolve();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
} else if (this.options.adminStatus && this._adminPort) {
|
|
1965
|
+
this.startAdminStatusLoop();
|
|
1966
|
+
}
|
|
1967
|
+
if (this._connectTimeoutTimer) {
|
|
1968
|
+
clearTimeout(this._connectTimeoutTimer);
|
|
1969
|
+
this._connectTimeoutTimer = null;
|
|
1970
|
+
}
|
|
1971
|
+
this._connectTimeoutTimer = setTimeout(() => {
|
|
1972
|
+
this._connectTimeoutTimer = null;
|
|
1973
|
+
if (!resolved) {
|
|
1974
|
+
resolved = true;
|
|
1975
|
+
this.log("frpc connection timeout \u2014 continuing (may still connect)");
|
|
1976
|
+
resolve();
|
|
1977
|
+
}
|
|
1978
|
+
}, 3e4);
|
|
1979
|
+
this._connectTimeoutTimer.unref?.();
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
/** Resolve the effective probe URL, honoring probeUrl > probePath. */
|
|
1983
|
+
resolveProbeUrl() {
|
|
1984
|
+
if (this.options.probeUrl) return this.options.probeUrl;
|
|
1985
|
+
if (this.options.probePath) {
|
|
1986
|
+
const base = this.getUrls().get(this.options.ports[0]);
|
|
1987
|
+
if (!base) return null;
|
|
1988
|
+
const sep = this.options.probePath.startsWith("/") ? "" : "/";
|
|
1989
|
+
return `${base}${sep}${this.options.probePath}`;
|
|
1990
|
+
}
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Start an end-to-end probe loop against the resolved probe URL.
|
|
1995
|
+
* Updates _probeOk + timestamps and fires onProbeFail on transitions.
|
|
1996
|
+
* Called from connect(); cleaned up in destroy().
|
|
1997
|
+
*/
|
|
1998
|
+
startProbeLoop(opts = {}) {
|
|
1999
|
+
const probeUrl = this.resolveProbeUrl();
|
|
2000
|
+
if (!probeUrl) return;
|
|
2001
|
+
this.stopProbeLoop();
|
|
2002
|
+
const intervalMs = this.options.probeIntervalMs ?? 3e4;
|
|
2003
|
+
let firstOkFired = false;
|
|
2004
|
+
let inFlight = false;
|
|
2005
|
+
const runProbe = async () => {
|
|
2006
|
+
if (this._destroyed || inFlight) return;
|
|
2007
|
+
inFlight = true;
|
|
2008
|
+
try {
|
|
2009
|
+
const ctrl = new AbortController();
|
|
2010
|
+
const timer = setTimeout(() => ctrl.abort(), 1e4);
|
|
2011
|
+
let resp;
|
|
2012
|
+
try {
|
|
2013
|
+
resp = await fetch(probeUrl, {
|
|
2014
|
+
method: "GET",
|
|
2015
|
+
signal: ctrl.signal,
|
|
2016
|
+
// Don't follow redirects — we just want any 2xx/3xx.
|
|
2017
|
+
redirect: "manual",
|
|
2018
|
+
headers: { "User-Agent": "svamp-frpc-probe/1" }
|
|
2019
|
+
});
|
|
2020
|
+
} finally {
|
|
2021
|
+
clearTimeout(timer);
|
|
2022
|
+
}
|
|
2023
|
+
if (resp.status < 400) {
|
|
2024
|
+
const wasFailing = !this._probeOk;
|
|
2025
|
+
this._probeOk = true;
|
|
2026
|
+
this._lastProbeOkAt = Date.now();
|
|
2027
|
+
if (wasFailing) {
|
|
2028
|
+
this.log(`probe ok: ${probeUrl} (${resp.status})`);
|
|
2029
|
+
}
|
|
2030
|
+
if (!firstOkFired && opts.onFirstOk) {
|
|
2031
|
+
firstOkFired = true;
|
|
2032
|
+
opts.onFirstOk();
|
|
2033
|
+
}
|
|
2034
|
+
} else {
|
|
2035
|
+
throw new Error(`probe got ${resp.status}`);
|
|
2036
|
+
}
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
const wasOk = this._probeOk;
|
|
2039
|
+
this._probeOk = false;
|
|
2040
|
+
this._lastProbeFailAt = Date.now();
|
|
2041
|
+
if (wasOk) {
|
|
2042
|
+
this.log(`probe failed: ${err?.message || err}`);
|
|
2043
|
+
this.options.onProbeFail?.(err instanceof Error ? err : new Error(String(err)));
|
|
2044
|
+
}
|
|
2045
|
+
} finally {
|
|
2046
|
+
inFlight = false;
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
const initialDelays = [500, 2e3, 5e3, 1e4];
|
|
2050
|
+
let burstIdx = 0;
|
|
2051
|
+
if (this._burstTimer) clearInterval(this._burstTimer);
|
|
2052
|
+
this._burstTimer = setInterval(() => {
|
|
2053
|
+
if (this._destroyed || firstOkFired || burstIdx >= initialDelays.length) {
|
|
2054
|
+
if (this._burstTimer) {
|
|
2055
|
+
clearInterval(this._burstTimer);
|
|
2056
|
+
this._burstTimer = null;
|
|
2057
|
+
}
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
burstIdx++;
|
|
2061
|
+
void runProbe();
|
|
2062
|
+
}, 1500);
|
|
2063
|
+
if (intervalMs > 0) {
|
|
2064
|
+
this._probeTimer = setInterval(runProbe, intervalMs);
|
|
2065
|
+
}
|
|
2066
|
+
void runProbe();
|
|
2067
|
+
}
|
|
2068
|
+
stopProbeLoop() {
|
|
2069
|
+
if (this._probeTimer) {
|
|
2070
|
+
clearInterval(this._probeTimer);
|
|
2071
|
+
this._probeTimer = null;
|
|
2072
|
+
}
|
|
2073
|
+
if (this._burstTimer) {
|
|
2074
|
+
clearInterval(this._burstTimer);
|
|
2075
|
+
this._burstTimer = null;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Poll frpc's loopback admin API (`/api/status`) to track whether this
|
|
2080
|
+
* tunnel's proxies are registered ("running"). Feeds the same `_probeOk` /
|
|
2081
|
+
* staleness fields the daemon health loop watches, so a ghosted/stuck proxy
|
|
2082
|
+
* is detected and recreated even with no HTTP health endpoint on the backend.
|
|
2083
|
+
*
|
|
2084
|
+
* Conservative semantics (critical infra — must not churn healthy tunnels):
|
|
2085
|
+
* - all proxies "running" → ok (refresh lastProbeOkAt)
|
|
2086
|
+
* - a proxy present but not running, or missing after a grace period
|
|
2087
|
+
* → fail (sets lastProbeFailAt; daemon recreates after staleness)
|
|
2088
|
+
* - admin API unreachable → INCONCLUSIVE: leave _probeOk untouched
|
|
2089
|
+
* (a transient blip can't flip ok→fail)
|
|
2090
|
+
*/
|
|
2091
|
+
startAdminStatusLoop() {
|
|
2092
|
+
this.stopAdminStatusLoop();
|
|
2093
|
+
if (!this._adminPort) return;
|
|
2094
|
+
const proxyNames = this.proxies.map((p) => p.name);
|
|
2095
|
+
const intervalMs = this.options.probeIntervalMs ?? 3e4;
|
|
2096
|
+
const startedAt = Date.now();
|
|
2097
|
+
const auth = "Basic " + Buffer.from(`${this._adminUser}:${this._adminPassword}`).toString("base64");
|
|
2098
|
+
this._probeOk = true;
|
|
2099
|
+
this._lastProbeOkAt = Date.now();
|
|
2100
|
+
let inFlight = false;
|
|
2101
|
+
const poll = async () => {
|
|
2102
|
+
if (this._destroyed || inFlight || !this.process) return;
|
|
2103
|
+
inFlight = true;
|
|
2104
|
+
try {
|
|
2105
|
+
const ctrl = new AbortController();
|
|
2106
|
+
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
2107
|
+
let payload;
|
|
2108
|
+
try {
|
|
2109
|
+
const resp = await fetch(`http://127.0.0.1:${this._adminPort}/api/status`, {
|
|
2110
|
+
headers: { Authorization: auth },
|
|
2111
|
+
signal: ctrl.signal
|
|
2112
|
+
});
|
|
2113
|
+
if (!resp.ok) throw new Error(`admin status ${resp.status}`);
|
|
2114
|
+
payload = await resp.json();
|
|
2115
|
+
} finally {
|
|
2116
|
+
clearTimeout(timer);
|
|
2117
|
+
}
|
|
2118
|
+
const { allRunning, missing, failed } = parseFrpcStatus(payload, proxyNames);
|
|
2119
|
+
const graceElapsed = Date.now() - startedAt > 2e4;
|
|
2120
|
+
if (allRunning || !failed.length && !graceElapsed) {
|
|
2121
|
+
const wasFailing = !this._probeOk;
|
|
2122
|
+
this._probeOk = true;
|
|
2123
|
+
this._lastProbeOkAt = Date.now();
|
|
2124
|
+
if (wasFailing) this.log(`admin status ok: all proxies running`);
|
|
2125
|
+
} else {
|
|
2126
|
+
const wasOk = this._probeOk;
|
|
2127
|
+
this._probeOk = false;
|
|
2128
|
+
this._lastProbeFailAt = Date.now();
|
|
2129
|
+
if (wasOk) {
|
|
2130
|
+
const detail = [
|
|
2131
|
+
...failed.map((f) => `${f.name}=${f.status}${f.err ? ` (${f.err})` : ""}`),
|
|
2132
|
+
...missing.map((m) => `${m}=missing`)
|
|
2133
|
+
].join(", ");
|
|
2134
|
+
this.log(`admin status fail: ${detail}`);
|
|
2135
|
+
this.options.onProbeFail?.(new Error(`frpc proxy not running: ${detail}`));
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
} catch {
|
|
2139
|
+
} finally {
|
|
2140
|
+
inFlight = false;
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
if (intervalMs > 0) this._statusTimer = setInterval(poll, intervalMs);
|
|
2144
|
+
setTimeout(() => void poll(), 3e3);
|
|
2145
|
+
}
|
|
2146
|
+
stopAdminStatusLoop() {
|
|
2147
|
+
if (this._statusTimer) {
|
|
2148
|
+
clearInterval(this._statusTimer);
|
|
2149
|
+
this._statusTimer = null;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/** Disconnect and stop the frpc process. */
|
|
2153
|
+
destroy() {
|
|
2154
|
+
this._destroyed = true;
|
|
2155
|
+
this._connected = false;
|
|
2156
|
+
this.stopProbeLoop();
|
|
2157
|
+
this.stopAdminStatusLoop();
|
|
2158
|
+
if (this._restartTimer) {
|
|
2159
|
+
clearTimeout(this._restartTimer);
|
|
2160
|
+
this._restartTimer = null;
|
|
2161
|
+
}
|
|
2162
|
+
if (this._connectTimeoutTimer) {
|
|
2163
|
+
clearTimeout(this._connectTimeoutTimer);
|
|
2164
|
+
this._connectTimeoutTimer = null;
|
|
2165
|
+
}
|
|
2166
|
+
if (this.process) {
|
|
2167
|
+
if (this.process.pid) _livingFrpcPids.delete(this.process.pid);
|
|
2168
|
+
this.process.kill("SIGTERM");
|
|
2169
|
+
const p = this.process;
|
|
2170
|
+
const killTimer = setTimeout(() => {
|
|
2171
|
+
try {
|
|
2172
|
+
if (p.exitCode === null && p.signalCode === null) p.kill("SIGKILL");
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
}, 5e3);
|
|
2176
|
+
killTimer.unref?.();
|
|
2177
|
+
p.once("exit", () => clearTimeout(killTimer));
|
|
2178
|
+
this.process = null;
|
|
2179
|
+
}
|
|
2180
|
+
try {
|
|
2181
|
+
unlinkSync(this.configPath);
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
/** Whether the frpc tunnel is connected. */
|
|
2186
|
+
get isConnected() {
|
|
2187
|
+
return this._connected && this.process !== null;
|
|
2188
|
+
}
|
|
2189
|
+
/** Snapshot of tunnel health for monitoring. */
|
|
2190
|
+
get status() {
|
|
2191
|
+
return {
|
|
2192
|
+
connected: this.isConnected,
|
|
2193
|
+
pid: this.process?.pid ?? null,
|
|
2194
|
+
name: this.options.name,
|
|
2195
|
+
ports: this.options.ports,
|
|
2196
|
+
serverAddr: this.serverConfig.serverAddr,
|
|
2197
|
+
recentLogs: this.logBuffer.slice(-10),
|
|
2198
|
+
consecutiveErrors: this._consecutiveErrors,
|
|
2199
|
+
lastConnectedAt: this._lastConnectedAt,
|
|
2200
|
+
lastErrorAt: this._lastErrorAt,
|
|
2201
|
+
firstErrorAt: this._firstErrorAt,
|
|
2202
|
+
restartAttempts: this._restartAttempts,
|
|
2203
|
+
failingDurationMs: this._firstErrorAt > 0 ? Date.now() - this._firstErrorAt : 0,
|
|
2204
|
+
probe: this.resolveProbeUrl() || this.options.adminStatus && this._adminPort ? {
|
|
2205
|
+
url: this.resolveProbeUrl() || `frpc-admin:${this._adminPort}/api/status`,
|
|
2206
|
+
ok: this._probeOk,
|
|
2207
|
+
lastOkAt: this._lastProbeOkAt,
|
|
2208
|
+
lastFailAt: this._lastProbeFailAt,
|
|
2209
|
+
stalenessMs: this._lastProbeOkAt > 0 ? Date.now() - this._lastProbeOkAt : 0
|
|
2210
|
+
} : void 0
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
/** Update the Hypha token. Rewrites config; takes effect on next frpc restart. */
|
|
2214
|
+
updateToken(newToken) {
|
|
2215
|
+
this.serverConfig.hyphaToken = newToken;
|
|
2216
|
+
const admin = this.options.adminStatus && this._adminPort ? { port: this._adminPort, user: this._adminUser, password: this._adminPassword } : void 0;
|
|
2217
|
+
const configContent = generateFrpcConfig(this.serverConfig, this.proxies, admin);
|
|
2218
|
+
writeFileSync(this.configPath, configContent, { mode: 384 });
|
|
2219
|
+
this.log("Config updated with fresh token");
|
|
2220
|
+
}
|
|
2221
|
+
/** Get the public URLs for each port. */
|
|
2222
|
+
getUrls() {
|
|
2223
|
+
const urls = /* @__PURE__ */ new Map();
|
|
2224
|
+
const base = this.serverConfig.subDomainHost || "svc.hypha.amun.ai";
|
|
2225
|
+
for (const proxy of this.proxies) {
|
|
2226
|
+
const subdomain = proxy.subdomain || proxy.name;
|
|
2227
|
+
urls.set(proxy.localPort, `https://${subdomain}.${base}`);
|
|
2228
|
+
}
|
|
2229
|
+
return urls;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
async function runFrpcTunnel(name, ports, serverConfig, tunnelOptions) {
|
|
2233
|
+
const portList = ports.join(", ");
|
|
2234
|
+
const tunnel = new FrpcTunnel({
|
|
2235
|
+
name,
|
|
2236
|
+
ports,
|
|
2237
|
+
serverConfig,
|
|
2238
|
+
...tunnelOptions,
|
|
2239
|
+
onConnect: () => {
|
|
2240
|
+
console.log(`Tunnel connected: ${name} \u2192 localhost:[${portList}]`);
|
|
2241
|
+
const urls = tunnel.getUrls();
|
|
2242
|
+
for (const [port, url] of urls) {
|
|
2243
|
+
console.log(` ${port} \u2192 ${url}`);
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
onDisconnect: () => {
|
|
2247
|
+
console.log("Tunnel disconnected, frpc will auto-reconnect...");
|
|
2248
|
+
},
|
|
2249
|
+
onError: (err) => {
|
|
2250
|
+
console.error(`Tunnel error: ${err.message}`);
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
const cleanup = () => {
|
|
2254
|
+
console.log("\nTunnel shutting down...");
|
|
2255
|
+
tunnel.destroy();
|
|
2256
|
+
process.exit(0);
|
|
2257
|
+
};
|
|
2258
|
+
process.on("SIGINT", cleanup);
|
|
2259
|
+
process.on("SIGTERM", cleanup);
|
|
2260
|
+
try {
|
|
2261
|
+
await tunnel.connect();
|
|
2262
|
+
console.log(`Tunnel is active. Press Ctrl+C to stop.`);
|
|
2263
|
+
await new Promise(() => {
|
|
2264
|
+
});
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
console.error(`Failed to establish tunnel: ${err.message}`);
|
|
2267
|
+
process.exit(1);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
var frpc = /*#__PURE__*/Object.freeze({
|
|
2272
|
+
__proto__: null,
|
|
2273
|
+
FrpcTunnel: FrpcTunnel,
|
|
2274
|
+
ensureFrpc: ensureFrpc,
|
|
2275
|
+
generateFrpcConfig: generateFrpcConfig,
|
|
2276
|
+
getFreePort: getFreePort,
|
|
2277
|
+
isFrpcSubdomainConflict: isFrpcSubdomainConflict,
|
|
2278
|
+
killOrphanedFrpc: killOrphanedFrpc,
|
|
2279
|
+
parseFrpcStatus: parseFrpcStatus,
|
|
2280
|
+
runFrpcTunnel: runFrpcTunnel,
|
|
2281
|
+
selectOrphanedFrpcPids: selectOrphanedFrpcPids,
|
|
2282
|
+
shouldGiveUpOnConflict: shouldGiveUpOnConflict
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
const MIME = {
|
|
2286
|
+
".html": "text/html; charset=utf-8",
|
|
2287
|
+
".htm": "text/html; charset=utf-8",
|
|
2288
|
+
".js": "text/javascript; charset=utf-8",
|
|
2289
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
2290
|
+
".css": "text/css; charset=utf-8",
|
|
2291
|
+
".json": "application/json; charset=utf-8",
|
|
2292
|
+
".map": "application/json; charset=utf-8",
|
|
2293
|
+
".txt": "text/plain; charset=utf-8",
|
|
2294
|
+
".md": "text/plain; charset=utf-8",
|
|
2295
|
+
".xml": "application/xml; charset=utf-8",
|
|
2296
|
+
".csv": "text/csv; charset=utf-8",
|
|
2297
|
+
".svg": "image/svg+xml",
|
|
2298
|
+
".png": "image/png",
|
|
2299
|
+
".jpg": "image/jpeg",
|
|
2300
|
+
".jpeg": "image/jpeg",
|
|
2301
|
+
".gif": "image/gif",
|
|
2302
|
+
".webp": "image/webp",
|
|
2303
|
+
".avif": "image/avif",
|
|
2304
|
+
".ico": "image/x-icon",
|
|
2305
|
+
".bmp": "image/bmp",
|
|
2306
|
+
".mp4": "video/mp4",
|
|
2307
|
+
".webm": "video/webm",
|
|
2308
|
+
".mov": "video/quicktime",
|
|
2309
|
+
".m4v": "video/mp4",
|
|
2310
|
+
".mp3": "audio/mpeg",
|
|
2311
|
+
".wav": "audio/wav",
|
|
2312
|
+
".ogg": "audio/ogg",
|
|
2313
|
+
".m4a": "audio/mp4",
|
|
2314
|
+
".flac": "audio/flac",
|
|
2315
|
+
".pdf": "application/pdf",
|
|
2316
|
+
".wasm": "application/wasm",
|
|
2317
|
+
".zip": "application/zip",
|
|
2318
|
+
".gz": "application/gzip",
|
|
2319
|
+
".tar": "application/x-tar",
|
|
2320
|
+
".woff": "font/woff",
|
|
2321
|
+
".woff2": "font/woff2",
|
|
2322
|
+
".ttf": "font/ttf",
|
|
2323
|
+
".otf": "font/otf",
|
|
2324
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2325
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2326
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
2327
|
+
};
|
|
2328
|
+
function contentTypeFor(filePath) {
|
|
2329
|
+
return MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
2330
|
+
}
|
|
2331
|
+
function hasBlockedDotSegment(relPath) {
|
|
2332
|
+
const decoded = safeDecode(relPath.replace(/^\/+/, ""));
|
|
2333
|
+
for (const seg of decoded.split("/")) {
|
|
2334
|
+
if (!seg || seg === "." || seg === "..") continue;
|
|
2335
|
+
if (seg.startsWith(".") && seg !== ".well-known") return true;
|
|
2336
|
+
}
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
function containedPath(rootDir, relPath) {
|
|
2340
|
+
const base = path.resolve(rootDir);
|
|
2341
|
+
const decoded = safeDecode(relPath);
|
|
2342
|
+
const fp = path.resolve(path.join(base, decoded));
|
|
2343
|
+
if (fp !== base && !fp.startsWith(base + path.sep)) return null;
|
|
2344
|
+
return fp;
|
|
2345
|
+
}
|
|
2346
|
+
function realContainedPath(rootDir, candidate) {
|
|
2347
|
+
try {
|
|
2348
|
+
const realRoot = fs.realpathSync(path.resolve(rootDir));
|
|
2349
|
+
const real = fs.realpathSync(candidate);
|
|
2350
|
+
if (real === realRoot || real.startsWith(realRoot + path.sep)) return real;
|
|
2351
|
+
return null;
|
|
2352
|
+
} catch {
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function safeDecode(p) {
|
|
2357
|
+
try {
|
|
2358
|
+
return decodeURIComponent(p);
|
|
2359
|
+
} catch {
|
|
2360
|
+
return p;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
function parseRange(header, size) {
|
|
2364
|
+
if (!header) return null;
|
|
2365
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
2366
|
+
if (!m) return null;
|
|
2367
|
+
const [, s, e] = m;
|
|
2368
|
+
if (s === "" && e === "") return null;
|
|
2369
|
+
let start, end;
|
|
2370
|
+
if (s === "") {
|
|
2371
|
+
const n = parseInt(e, 10);
|
|
2372
|
+
if (n <= 0) return "unsatisfiable";
|
|
2373
|
+
start = Math.max(0, size - n);
|
|
2374
|
+
end = size - 1;
|
|
2375
|
+
} else {
|
|
2376
|
+
start = parseInt(s, 10);
|
|
2377
|
+
end = e === "" ? size - 1 : parseInt(e, 10);
|
|
2378
|
+
}
|
|
2379
|
+
if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) return "unsatisfiable";
|
|
2380
|
+
if (end >= size) end = size - 1;
|
|
2381
|
+
return { start, end };
|
|
2382
|
+
}
|
|
2383
|
+
function directoryListingHtml(mountRelUrl, entries) {
|
|
2384
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2385
|
+
const base = mountRelUrl.endsWith("/") ? mountRelUrl : mountRelUrl + "/";
|
|
2386
|
+
const rows = entries.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1).map((e) => {
|
|
2387
|
+
const href = base + encodeURIComponent(e.name) + (e.isDir ? "/" : "");
|
|
2388
|
+
return `<li><a href="${esc(href)}">${esc(e.name)}${e.isDir ? "/" : ""}</a></li>`;
|
|
2389
|
+
}).join("\n");
|
|
2390
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(base)}</title>
|
|
2391
|
+
<style>body{font-family:system-ui,sans-serif;max-width:760px;margin:40px auto;padding:0 20px;color:#333}
|
|
2392
|
+
h1{font-size:1.15rem;word-break:break-all}ul{list-style:none;padding:0}li{padding:6px 0;border-bottom:1px solid #eee}
|
|
2393
|
+
a{color:#0969da;text-decoration:none}a:hover{text-decoration:underline}</style></head>
|
|
2394
|
+
<body><h1>${esc(base)}</h1><ul>${rows || "<li>(empty)</li>"}</ul></body></html>`;
|
|
2395
|
+
}
|
|
2396
|
+
const CORS = {
|
|
2397
|
+
"Access-Control-Allow-Origin": "*",
|
|
2398
|
+
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
|
2399
|
+
"Access-Control-Allow-Headers": "*"
|
|
2400
|
+
};
|
|
2401
|
+
function serveStaticMount(req, res, opts) {
|
|
2402
|
+
const { rootDir, relPath, mountUrlPrefix } = opts;
|
|
2403
|
+
const browse = opts.browse !== false;
|
|
2404
|
+
const method = (req.method || "GET").toUpperCase();
|
|
2405
|
+
if (method === "OPTIONS") {
|
|
2406
|
+
res.writeHead(204, CORS);
|
|
2407
|
+
res.end();
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2411
|
+
res.writeHead(405, { ...CORS, Allow: "GET, HEAD, OPTIONS" });
|
|
2412
|
+
res.end("Method Not Allowed");
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
let rootStat;
|
|
2416
|
+
try {
|
|
2417
|
+
rootStat = fs.statSync(rootDir);
|
|
2418
|
+
} catch {
|
|
2419
|
+
res.writeHead(404, CORS);
|
|
2420
|
+
res.end("Not Found");
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
if (rootStat.isFile()) {
|
|
2424
|
+
sendFile(req, res, rootDir, rootStat);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (hasBlockedDotSegment(relPath)) {
|
|
2428
|
+
res.writeHead(404, CORS);
|
|
2429
|
+
res.end("Not Found");
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const lexTarget = containedPath(rootDir, relPath.replace(/^\/+/, ""));
|
|
2433
|
+
if (!lexTarget) {
|
|
2434
|
+
res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
|
|
2435
|
+
res.end("Forbidden");
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const target = realContainedPath(rootDir, lexTarget);
|
|
2439
|
+
if (!target) {
|
|
2440
|
+
res.writeHead(404, { ...CORS, "Content-Type": "text/plain" });
|
|
2441
|
+
res.end("Not Found");
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
let st;
|
|
2445
|
+
try {
|
|
2446
|
+
st = fs.statSync(target);
|
|
2447
|
+
} catch {
|
|
2448
|
+
res.writeHead(404, { ...CORS, "Content-Type": "text/plain" });
|
|
2449
|
+
res.end("Not Found");
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (st.isDirectory()) {
|
|
2453
|
+
const reqPath = (req.url || "/").split("?")[0];
|
|
2454
|
+
if (!reqPath.endsWith("/")) {
|
|
2455
|
+
res.writeHead(302, { ...CORS, Location: reqPath + "/" });
|
|
2456
|
+
res.end();
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
const indexPath = realContainedPath(rootDir, path.join(target, "index.html"));
|
|
2460
|
+
if (indexPath) {
|
|
2461
|
+
try {
|
|
2462
|
+
const ist = fs.statSync(indexPath);
|
|
2463
|
+
if (ist.isFile()) {
|
|
2464
|
+
sendFile(req, res, indexPath, ist);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (!browse) {
|
|
2471
|
+
res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
|
|
2472
|
+
res.end("Directory listing disabled");
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
let entries;
|
|
2476
|
+
try {
|
|
2477
|
+
entries = fs.readdirSync(target, { withFileTypes: true }).filter((d) => !d.name.startsWith(".")).map((d) => ({ name: d.name, isDir: d.isDirectory() }));
|
|
2478
|
+
} catch {
|
|
2479
|
+
res.writeHead(500, CORS);
|
|
2480
|
+
res.end("Listing failed");
|
|
1439
2481
|
return;
|
|
1440
2482
|
}
|
|
1441
2483
|
const listUrl = mountUrlPrefix + (relPath === "/" ? "/" : relPath);
|
|
@@ -1566,6 +2608,8 @@ class TokenCache {
|
|
|
1566
2608
|
this.cache.delete(token);
|
|
1567
2609
|
return null;
|
|
1568
2610
|
}
|
|
2611
|
+
this.cache.delete(token);
|
|
2612
|
+
this.cache.set(token, info);
|
|
1569
2613
|
return info;
|
|
1570
2614
|
}
|
|
1571
2615
|
set(token, email) {
|
|
@@ -1853,241 +2897,60 @@ btn.addEventListener('click', async () => {
|
|
|
1853
2897
|
}
|
|
1854
2898
|
/**
|
|
1855
2899
|
* Render an "access denied" page for a request that IS authenticated (valid
|
|
1856
|
-
* Hypha token) but whose email is not authorized for the mount — or for a
|
|
1857
|
-
* misconfigured owner mount with no resolvable owner. This is served as a
|
|
1858
|
-
* 403; it deliberately does NOT redirect to the login page, which would
|
|
1859
|
-
* loop forever and tell the user nothing about why they were rejected.
|
|
1860
|
-
*/
|
|
1861
|
-
getAccessDeniedHtml(opts) {
|
|
1862
|
-
const { email, access, ownerEmail, misconfigured } = opts;
|
|
1863
|
-
const loginHref = `/__login__?return=${encodeURIComponent(opts.returnUrl || "/")}`;
|
|
1864
|
-
let requirement;
|
|
1865
|
-
if (misconfigured) {
|
|
1866
|
-
requirement = `This resource is set to <b>owner-only</b> access, but the server never resolved an owner email (the daemon authenticated with a token that has no email claim). No account can satisfy this \u2014 it is a server-side misconfiguration, not a problem with your account. Fix it by re-creating the mount with an explicit email allowlist, e.g. <code>access: ["${escapeHtml(email || "you@example.com")}"]</code>.`;
|
|
1867
|
-
} else if (access === "owner") {
|
|
1868
|
-
requirement = `This resource is restricted to its owner${ownerEmail ? ` (<code>${escapeHtml(ownerEmail)}</code>)` : ""}.`;
|
|
1869
|
-
} else if (Array.isArray(access)) {
|
|
1870
|
-
const list = access.map((e) => `<code>${escapeHtml(e)}</code>`).join(", ");
|
|
1871
|
-
requirement = `This resource is restricted to: ${list}.`;
|
|
1872
|
-
} else {
|
|
1873
|
-
requirement = "This resource is restricted.";
|
|
1874
|
-
}
|
|
1875
|
-
const signedInLine = email ? `You are signed in as <code>${escapeHtml(email)}</code>, which is not on the allow list.` : `You are not signed in.`;
|
|
1876
|
-
return `<!DOCTYPE html>
|
|
1877
|
-
<html><head>
|
|
1878
|
-
<meta charset="utf-8">
|
|
1879
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1880
|
-
<title>Access denied \u2014 Svamp File Server</title>
|
|
1881
|
-
<style>
|
|
1882
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
1883
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f6f8fa;color:#24292f;padding:16px}
|
|
1884
|
-
.card{background:#fff;border:1px solid #d0d7de;border-radius:12px;padding:32px;max-width:480px;width:100%;box-shadow:0 4px 12px rgba(31,35,40,0.06)}
|
|
1885
|
-
h1{font-size:1.25rem;margin-bottom:8px}
|
|
1886
|
-
.lead{color:#cf222e;font-weight:600;margin-bottom:16px}
|
|
1887
|
-
p{color:#3d444d;font-size:0.92rem;line-height:1.5;margin-bottom:14px}
|
|
1888
|
-
code{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:1px 6px;font-size:0.85em}
|
|
1889
|
-
a.btn{display:inline-block;background:#0969da;color:#fff;border-radius:8px;padding:10px 20px;font-weight:500;text-decoration:none;margin-top:4px}
|
|
1890
|
-
a.btn:hover{background:#0860c4}
|
|
1891
|
-
</style>
|
|
1892
|
-
</head><body>
|
|
1893
|
-
<div class="card">
|
|
1894
|
-
<h1>${misconfigured ? "\u26D4 Access not possible" : "\u26D4 Access denied"}</h1>
|
|
1895
|
-
<p class="lead">${signedInLine}</p>
|
|
1896
|
-
<p>${requirement}</p>
|
|
1897
|
-
${misconfigured ? "" : `<a class="btn" href="${loginHref}">Sign in with a different account</a>`}
|
|
1898
|
-
</div>
|
|
1899
|
-
</body></html>`;
|
|
1900
|
-
}
|
|
1901
|
-
destroy() {
|
|
1902
|
-
this.cache.clear();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
function escapeHtml(s) {
|
|
1906
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
function svampHome() {
|
|
1910
|
-
return process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1911
|
-
}
|
|
1912
|
-
function cacheFile() {
|
|
1913
|
-
return join(svampHome(), "instance-config.json");
|
|
1914
|
-
}
|
|
1915
|
-
const CONFIG_FILENAME = "svamp.json";
|
|
1916
|
-
let _config = null;
|
|
1917
|
-
let _loaded = false;
|
|
1918
|
-
let _cacheChecked = false;
|
|
1919
|
-
function stripSlash$1(u) {
|
|
1920
|
-
return u.replace(/\/+$/, "");
|
|
1921
|
-
}
|
|
1922
|
-
function getInstanceUrl() {
|
|
1923
|
-
const raw = (process.env.SVAMP_INSTANCE_URL || "").trim();
|
|
1924
|
-
return raw ? stripSlash$1(raw) : void 0;
|
|
1925
|
-
}
|
|
1926
|
-
function configUrlsFor(anchor) {
|
|
1927
|
-
const base = stripSlash$1(anchor);
|
|
1928
|
-
return [`${base}/${CONFIG_FILENAME}`, `${base}/.well-known/${CONFIG_FILENAME}`];
|
|
1929
|
-
}
|
|
1930
|
-
async function fetchConfig(anchor) {
|
|
1931
|
-
for (const url of configUrlsFor(anchor)) {
|
|
1932
|
-
try {
|
|
1933
|
-
const resp = await fetch(url, { signal: AbortSignal.timeout(8e3) });
|
|
1934
|
-
if (!resp.ok) continue;
|
|
1935
|
-
const data = await resp.json();
|
|
1936
|
-
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
1937
|
-
return data;
|
|
1938
|
-
}
|
|
1939
|
-
} catch {
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
return null;
|
|
1943
|
-
}
|
|
1944
|
-
function readCache() {
|
|
1945
|
-
try {
|
|
1946
|
-
if (existsSync$1(cacheFile())) {
|
|
1947
|
-
const data = JSON.parse(readFileSync$1(cacheFile(), "utf-8"));
|
|
1948
|
-
if (data && typeof data === "object" && !Array.isArray(data)) return data;
|
|
1949
|
-
}
|
|
1950
|
-
} catch {
|
|
1951
|
-
}
|
|
1952
|
-
return null;
|
|
1953
|
-
}
|
|
1954
|
-
function writeCache(cfg) {
|
|
1955
|
-
try {
|
|
1956
|
-
mkdirSync(svampHome(), { recursive: true });
|
|
1957
|
-
writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
1958
|
-
} catch {
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
function applyConfig(cfg) {
|
|
1962
|
-
_config = cfg;
|
|
1963
|
-
if (cfg.hyphaServerUrl && typeof cfg.hyphaServerUrl === "string") {
|
|
1964
|
-
process.env.HYPHA_SERVER_URL = stripSlash$1(cfg.hyphaServerUrl.trim());
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
async function loadInstanceConfig(opts = {}) {
|
|
1968
|
-
if (_loaded && !opts.force) return _config;
|
|
1969
|
-
const anchor = opts.anchor ? stripSlash$1(opts.anchor) : getInstanceUrl();
|
|
1970
|
-
if (anchor) {
|
|
1971
|
-
const fetched = await fetchConfig(anchor);
|
|
1972
|
-
if (fetched) {
|
|
1973
|
-
applyConfig(fetched);
|
|
1974
|
-
writeCache(fetched);
|
|
1975
|
-
_loaded = true;
|
|
1976
|
-
_cacheChecked = true;
|
|
1977
|
-
return _config;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
const cached = readCache();
|
|
1981
|
-
if (cached) applyConfig(cached);
|
|
1982
|
-
_loaded = true;
|
|
1983
|
-
_cacheChecked = true;
|
|
1984
|
-
return _config;
|
|
1985
|
-
}
|
|
1986
|
-
function getLoadedConfig() {
|
|
1987
|
-
if (!_loaded && !_cacheChecked) {
|
|
1988
|
-
const cached = readCache();
|
|
1989
|
-
if (cached) applyConfig(cached);
|
|
1990
|
-
_cacheChecked = true;
|
|
1991
|
-
}
|
|
1992
|
-
return _config;
|
|
1993
|
-
}
|
|
1994
|
-
function clearInstanceConfigCache() {
|
|
1995
|
-
_config = null;
|
|
1996
|
-
_loaded = false;
|
|
1997
|
-
_cacheChecked = false;
|
|
1998
|
-
try {
|
|
1999
|
-
if (existsSync$1(cacheFile())) rmSync(cacheFile());
|
|
2000
|
-
} catch {
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
function __setConfigForTest(cfg) {
|
|
2004
|
-
_config = cfg;
|
|
2005
|
-
_loaded = true;
|
|
2006
|
-
_cacheChecked = true;
|
|
2007
|
-
if (cfg) applyConfig(cfg);
|
|
2008
|
-
}
|
|
2009
|
-
function __resetForTest() {
|
|
2010
|
-
_config = null;
|
|
2011
|
-
_loaded = false;
|
|
2012
|
-
_cacheChecked = false;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
var instanceConfig = /*#__PURE__*/Object.freeze({
|
|
2016
|
-
__proto__: null,
|
|
2017
|
-
CONFIG_FILENAME: CONFIG_FILENAME,
|
|
2018
|
-
__resetForTest: __resetForTest,
|
|
2019
|
-
__setConfigForTest: __setConfigForTest,
|
|
2020
|
-
clearInstanceConfigCache: clearInstanceConfigCache,
|
|
2021
|
-
configUrlsFor: configUrlsFor,
|
|
2022
|
-
getInstanceUrl: getInstanceUrl,
|
|
2023
|
-
getLoadedConfig: getLoadedConfig,
|
|
2024
|
-
loadInstanceConfig: loadInstanceConfig
|
|
2025
|
-
});
|
|
2026
|
-
|
|
2027
|
-
function stripSlash(u) {
|
|
2028
|
-
return u.replace(/\/+$/, "");
|
|
2029
|
-
}
|
|
2030
|
-
function getHyphaServerUrl() {
|
|
2031
|
-
const fromCfg = getLoadedConfig()?.hyphaServerUrl;
|
|
2032
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
2033
|
-
const env = (process.env.HYPHA_SERVER_URL || "").trim();
|
|
2034
|
-
return env ? stripSlash(env) : void 0;
|
|
2035
|
-
}
|
|
2036
|
-
function getFrpsSubdomainHost() {
|
|
2037
|
-
const fromCfg = getLoadedConfig()?.frps?.subdomainHost;
|
|
2038
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
2039
|
-
const env = (process.env.FRPS_SUBDOMAIN_HOST || "").trim();
|
|
2040
|
-
if (env) return env;
|
|
2041
|
-
throw new Error(
|
|
2042
|
-
'frps subdomain host is not configured. Serve a svamp.json with "frps.subdomainHost" at your frontend URL (svamp login <frontend-url>), or set FRPS_SUBDOMAIN_HOST (legacy). It is deployment-specific and never guessed.'
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
function getFrpsServerAddr() {
|
|
2046
|
-
const fromCfg = getLoadedConfig()?.frps?.serverAddr;
|
|
2047
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
2048
|
-
const env = (process.env.FRPS_SERVER_ADDR || "").trim();
|
|
2049
|
-
if (env) return env;
|
|
2050
|
-
return `frps-ctrl.${getFrpsSubdomainHost()}`;
|
|
2051
|
-
}
|
|
2052
|
-
function getFrpsServerPort() {
|
|
2053
|
-
const fromCfg = getLoadedConfig()?.frps?.serverPort;
|
|
2054
|
-
if (typeof fromCfg === "number" && Number.isFinite(fromCfg)) return fromCfg;
|
|
2055
|
-
const env = parseInt(process.env.FRPS_SERVER_PORT || "", 10);
|
|
2056
|
-
return Number.isFinite(env) ? env : void 0;
|
|
2057
|
-
}
|
|
2058
|
-
function getSvampWebBaseUrl() {
|
|
2059
|
-
const fromCfg = getLoadedConfig()?.web?.baseUrl;
|
|
2060
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
2061
|
-
const env = (process.env.SVAMP_WEB_URL || "").trim();
|
|
2062
|
-
if (env) return stripSlash(env);
|
|
2063
|
-
const instance = getInstanceUrl();
|
|
2064
|
-
if (instance) return instance;
|
|
2065
|
-
throw new Error(
|
|
2066
|
-
'Svamp web base URL is not configured. Serve a svamp.json with "web.baseUrl", log in with your frontend URL (svamp login <frontend-url>), or set SVAMP_WEB_URL (legacy).'
|
|
2067
|
-
);
|
|
2068
|
-
}
|
|
2069
|
-
function getSkillsServerUrl() {
|
|
2070
|
-
const fromCfg = getLoadedConfig()?.skills?.server;
|
|
2071
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return stripSlash(fromCfg.trim());
|
|
2072
|
-
const env = (process.env.HYPHA_SKILLS_SERVER || "").trim();
|
|
2073
|
-
if (env) return stripSlash(env);
|
|
2074
|
-
const server = getHyphaServerUrl();
|
|
2075
|
-
if (server) return server;
|
|
2076
|
-
throw new Error(
|
|
2077
|
-
"Cannot determine skills marketplace server. Serve a svamp.json or set HYPHA_SERVER_URL."
|
|
2078
|
-
);
|
|
2079
|
-
}
|
|
2080
|
-
function getSkillsWorkspace() {
|
|
2081
|
-
const fromCfg = getLoadedConfig()?.skills?.workspace;
|
|
2082
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
2083
|
-
return (process.env.HYPHA_SKILLS_WORKSPACE || "").trim() || "hypha-cloud";
|
|
2900
|
+
* Hypha token) but whose email is not authorized for the mount — or for a
|
|
2901
|
+
* misconfigured owner mount with no resolvable owner. This is served as a
|
|
2902
|
+
* 403; it deliberately does NOT redirect to the login page, which would
|
|
2903
|
+
* loop forever and tell the user nothing about why they were rejected.
|
|
2904
|
+
*/
|
|
2905
|
+
getAccessDeniedHtml(opts) {
|
|
2906
|
+
const { email, access, ownerEmail, misconfigured } = opts;
|
|
2907
|
+
const loginHref = `/__login__?return=${encodeURIComponent(opts.returnUrl || "/")}`;
|
|
2908
|
+
let requirement;
|
|
2909
|
+
if (misconfigured) {
|
|
2910
|
+
requirement = `This resource is set to <b>owner-only</b> access, but the server never resolved an owner email (the daemon authenticated with a token that has no email claim). No account can satisfy this \u2014 it is a server-side misconfiguration, not a problem with your account. Fix it by re-creating the mount with an explicit email allowlist, e.g. <code>access: ["${escapeHtml(email || "you@example.com")}"]</code>.`;
|
|
2911
|
+
} else if (access === "owner") {
|
|
2912
|
+
requirement = `This resource is restricted to its owner${ownerEmail ? ` (<code>${escapeHtml(ownerEmail)}</code>)` : ""}.`;
|
|
2913
|
+
} else if (Array.isArray(access)) {
|
|
2914
|
+
const list = access.map((e) => `<code>${escapeHtml(e)}</code>`).join(", ");
|
|
2915
|
+
requirement = `This resource is restricted to: ${list}.`;
|
|
2916
|
+
} else {
|
|
2917
|
+
requirement = "This resource is restricted.";
|
|
2918
|
+
}
|
|
2919
|
+
const signedInLine = email ? `You are signed in as <code>${escapeHtml(email)}</code>, which is not on the allow list.` : `You are not signed in.`;
|
|
2920
|
+
return `<!DOCTYPE html>
|
|
2921
|
+
<html><head>
|
|
2922
|
+
<meta charset="utf-8">
|
|
2923
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2924
|
+
<title>Access denied \u2014 Svamp File Server</title>
|
|
2925
|
+
<style>
|
|
2926
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2927
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#f6f8fa;color:#24292f;padding:16px}
|
|
2928
|
+
.card{background:#fff;border:1px solid #d0d7de;border-radius:12px;padding:32px;max-width:480px;width:100%;box-shadow:0 4px 12px rgba(31,35,40,0.06)}
|
|
2929
|
+
h1{font-size:1.25rem;margin-bottom:8px}
|
|
2930
|
+
.lead{color:#cf222e;font-weight:600;margin-bottom:16px}
|
|
2931
|
+
p{color:#3d444d;font-size:0.92rem;line-height:1.5;margin-bottom:14px}
|
|
2932
|
+
code{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:1px 6px;font-size:0.85em}
|
|
2933
|
+
a.btn{display:inline-block;background:#0969da;color:#fff;border-radius:8px;padding:10px 20px;font-weight:500;text-decoration:none;margin-top:4px}
|
|
2934
|
+
a.btn:hover{background:#0860c4}
|
|
2935
|
+
</style>
|
|
2936
|
+
</head><body>
|
|
2937
|
+
<div class="card">
|
|
2938
|
+
<h1>${misconfigured ? "\u26D4 Access not possible" : "\u26D4 Access denied"}</h1>
|
|
2939
|
+
<p class="lead">${signedInLine}</p>
|
|
2940
|
+
<p>${requirement}</p>
|
|
2941
|
+
${misconfigured ? "" : `<a class="btn" href="${loginHref}">Sign in with a different account</a>`}
|
|
2942
|
+
</div>
|
|
2943
|
+
</body></html>`;
|
|
2944
|
+
}
|
|
2945
|
+
destroy() {
|
|
2946
|
+
this.cache.clear();
|
|
2947
|
+
}
|
|
2084
2948
|
}
|
|
2085
|
-
function
|
|
2086
|
-
|
|
2087
|
-
if (typeof fromCfg === "string" && fromCfg.trim()) return fromCfg.trim();
|
|
2088
|
-
return (process.env.HYPHA_SKILLS_COLLECTION || "").trim() || "marketplace";
|
|
2949
|
+
function escapeHtml(s) {
|
|
2950
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2089
2951
|
}
|
|
2090
2952
|
|
|
2953
|
+
const SESSION_FILES_MOUNT = "svamp-session-files";
|
|
2091
2954
|
function generateLinkToken() {
|
|
2092
2955
|
return crypto.randomBytes(11).toString("hex");
|
|
2093
2956
|
}
|
|
@@ -2267,6 +3130,8 @@ class ServeManager {
|
|
|
2267
3130
|
managedPidDir;
|
|
2268
3131
|
log;
|
|
2269
3132
|
hyphaServerUrl;
|
|
3133
|
+
/** #126: daemon-provided lookup of sessionId → { cwd, sharing, ownerEmail } for the files gateway. */
|
|
3134
|
+
sessionResolver = null;
|
|
2270
3135
|
constructor(svampHome, logger, hyphaServerUrl) {
|
|
2271
3136
|
this.persistFile = path.join(svampHome, "serve-mounts.json");
|
|
2272
3137
|
this.managedPidDir = path.join(svampHome, "serve-managed");
|
|
@@ -2279,6 +3144,60 @@ class ServeManager {
|
|
|
2279
3144
|
this.auth = new ServeAuth({ hyphaServerUrl: this.hyphaServerUrl });
|
|
2280
3145
|
}
|
|
2281
3146
|
// ── Public API ───────────────────────────────────────────────────────
|
|
3147
|
+
/**
|
|
3148
|
+
* #126: register the daemon's sessionId → { cwd, sharing, ownerEmail } lookup used by the
|
|
3149
|
+
* session-files gateway to resolve + authorize each request. Call once at daemon start.
|
|
3150
|
+
*/
|
|
3151
|
+
setSessionResolver(fn) {
|
|
3152
|
+
this.sessionResolver = fn;
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* #126: bring up the always-on, per-machine session-files gateway. Idempotent. `stableToken`
|
|
3156
|
+
* must be a stable per-machine hex/alnum string (DNS-label-safe) so the gateway's subdomain is
|
|
3157
|
+
* unique across machines AND stable across daemon restarts. The token is NOT a capability —
|
|
3158
|
+
* every request is authorized per-session inside handleSessionFilesRequest. Returns the gateway
|
|
3159
|
+
* base URL (`https://static-svamp-session-files-<token>.svc…/`) or null if the tunnel isn't up.
|
|
3160
|
+
*/
|
|
3161
|
+
async ensureSessionFilesGateway(stableToken) {
|
|
3162
|
+
if (!stableToken || !/^[a-z0-9-]{8,}$/i.test(stableToken)) {
|
|
3163
|
+
throw new Error("ensureSessionFilesGateway: stableToken must be a DNS-label-safe string (\u22658 alnum/hyphen chars)");
|
|
3164
|
+
}
|
|
3165
|
+
const token = stableToken.toLowerCase();
|
|
3166
|
+
const existing = this.mounts.get(SESSION_FILES_MOUNT);
|
|
3167
|
+
if (!existing?.sessionFilesGateway || existing.linkToken !== token) {
|
|
3168
|
+
this.mounts.set(SESSION_FILES_MOUNT, {
|
|
3169
|
+
name: SESSION_FILES_MOUNT,
|
|
3170
|
+
access: "link",
|
|
3171
|
+
linkToken: token,
|
|
3172
|
+
sessionFilesGateway: true,
|
|
3173
|
+
addedAt: Date.now()
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
await this.ensureRunning();
|
|
3177
|
+
if (!this.mountTunnels.has(SESSION_FILES_MOUNT)) {
|
|
3178
|
+
await this.startMountTunnel(SESSION_FILES_MOUNT);
|
|
3179
|
+
}
|
|
3180
|
+
const base = this.getSessionFilesBaseUrl();
|
|
3181
|
+
this.log(`Session-files gateway ready: ${base || "(tunnel pending)"}`);
|
|
3182
|
+
return base;
|
|
3183
|
+
}
|
|
3184
|
+
/** #126: base URL of the session-files gateway, or null when the gateway/tunnel isn't up. */
|
|
3185
|
+
getSessionFilesBaseUrl() {
|
|
3186
|
+
const mount = this.mounts.get(SESSION_FILES_MOUNT);
|
|
3187
|
+
if (!mount?.sessionFilesGateway) return null;
|
|
3188
|
+
return this.getMountUrl(SESSION_FILES_MOUNT);
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* #126: the per-session file endpoint URL — `<base>s/<sessionId>/`. The frontend appends a
|
|
3192
|
+
* relative file path and issues GET (download, Range-streamed) / PUT (upload, streamed) /
|
|
3193
|
+
* DELETE / GET?list=1 (JSON dir listing). Null when the gateway tunnel isn't up (→ RPC fallback).
|
|
3194
|
+
*/
|
|
3195
|
+
getSessionFilesEndpoint(sessionId) {
|
|
3196
|
+
const base = this.getSessionFilesBaseUrl();
|
|
3197
|
+
if (!base) return null;
|
|
3198
|
+
const b = base.endsWith("/") ? base : `${base}/`;
|
|
3199
|
+
return `${b}s/${encodeURIComponent(sessionId)}/`;
|
|
3200
|
+
}
|
|
2282
3201
|
/**
|
|
2283
3202
|
* Add a static mount (backward-compatible thin wrapper around applyMount).
|
|
2284
3203
|
* Throws if a mount with the same name already exists, preserving the
|
|
@@ -2384,7 +3303,7 @@ class ServeManager {
|
|
|
2384
3303
|
* List mounts, optionally filtered by sessionId.
|
|
2385
3304
|
*/
|
|
2386
3305
|
listMounts(sessionId) {
|
|
2387
|
-
const all = Array.from(this.mounts.values());
|
|
3306
|
+
const all = Array.from(this.mounts.values()).filter((m) => !m.sessionFilesGateway);
|
|
2388
3307
|
const filtered = sessionId ? all.filter((m) => m.sessionId === sessionId) : all;
|
|
2389
3308
|
return filtered.map((m) => ({ ...m, url: this.getMountUrl(m.name) || void 0 }));
|
|
2390
3309
|
}
|
|
@@ -2402,8 +3321,9 @@ class ServeManager {
|
|
|
2402
3321
|
port: running ? this.port : 0,
|
|
2403
3322
|
authProxyPort: running ? this.port : 0,
|
|
2404
3323
|
running,
|
|
2405
|
-
|
|
2406
|
-
|
|
3324
|
+
// #126: exclude the internal session-files gateway from user-facing counts/listings.
|
|
3325
|
+
mountCount: Array.from(this.mounts.values()).filter((m) => !m.sessionFilesGateway).length,
|
|
3326
|
+
mounts: Array.from(this.mounts.values()).filter((m) => !m.sessionFilesGateway).map((m) => ({
|
|
2407
3327
|
...m,
|
|
2408
3328
|
url: this.getMountUrl(m.name) || void 0
|
|
2409
3329
|
}))
|
|
@@ -2717,7 +3637,9 @@ class ServeManager {
|
|
|
2717
3637
|
}
|
|
2718
3638
|
persist() {
|
|
2719
3639
|
const state = {
|
|
2720
|
-
|
|
3640
|
+
// #126: the session-files gateway is recreated fresh at daemon start (directory-less,
|
|
3641
|
+
// token owned by the daemon) — never persist it.
|
|
3642
|
+
mounts: Array.from(this.mounts.values()).filter((m) => !m.sessionFilesGateway),
|
|
2721
3643
|
...this.port > 0 ? { port: this.port } : {}
|
|
2722
3644
|
};
|
|
2723
3645
|
try {
|
|
@@ -2737,6 +3659,7 @@ class ServeManager {
|
|
|
2737
3659
|
if (this.proxyServer) return;
|
|
2738
3660
|
killOrphanedCaddy((m) => this.log(m));
|
|
2739
3661
|
reapOrphanedManagedProcesses(this.managedPidDir, (m) => this.log(m));
|
|
3662
|
+
killOrphanedFrpc((m) => this.log(m));
|
|
2740
3663
|
this.port = await tryReservePort(this.persistedPort);
|
|
2741
3664
|
if (this.persistedPort && this.port !== this.persistedPort) {
|
|
2742
3665
|
this.log(`\u26A0 Previous serve port ${this.persistedPort} unavailable \u2014 using ${this.port}. Downstream configs referencing the old port will need updating.`);
|
|
@@ -2754,6 +3677,195 @@ h1{font-size:1.3rem}ul{list-style:none;padding:0}li{padding:8px 0;border-bottom:
|
|
|
2754
3677
|
a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:underline}</style></head>
|
|
2755
3678
|
<body><h1>Svamp File Server</h1><ul>${items || "<li>No mounts registered.</li>"}</ul></body></html>`;
|
|
2756
3679
|
}
|
|
3680
|
+
/**
|
|
3681
|
+
* #126: handle a request to the session-files gateway. Routes `/s/<sessionId>/<relPath>`:
|
|
3682
|
+
* OPTIONS → CORS preflight (GET/HEAD/PUT/DELETE)
|
|
3683
|
+
* GET/HEAD → download (Range/206 streamed via serveStaticMount)
|
|
3684
|
+
* GET …?list=1 → JSON directory listing [{name,isDir,size,mtime}]
|
|
3685
|
+
* PUT → streamed upload to a temp file + atomic rename (large-file safe)
|
|
3686
|
+
* DELETE → unlink
|
|
3687
|
+
*
|
|
3688
|
+
* Auth (live boundary): authenticate the requester's email via ServeAuth (parse_token), then
|
|
3689
|
+
* authorize against THAT session's owner/sharing at role 'admin' — mirroring the RPC file ops.
|
|
3690
|
+
* CRITICAL: unlike the RPC path (which relies on Hypha workspace isolation for null sharing),
|
|
3691
|
+
* this is a PUBLIC HTTP ingress, so it FAILS CLOSED: a null/disabled sharing config admits ONLY
|
|
3692
|
+
* the session owner. No token → 401; valid token but not owner/admin → 403.
|
|
3693
|
+
*/
|
|
3694
|
+
async handleSessionFilesRequest(req, res, url, basePath) {
|
|
3695
|
+
const CORS = {
|
|
3696
|
+
"Access-Control-Allow-Origin": "*",
|
|
3697
|
+
"Access-Control-Allow-Methods": "GET, HEAD, PUT, DELETE, OPTIONS",
|
|
3698
|
+
"Access-Control-Allow-Headers": "*",
|
|
3699
|
+
"Access-Control-Expose-Headers": "Content-Length, Content-Range, ETag",
|
|
3700
|
+
"Access-Control-Max-Age": "600"
|
|
3701
|
+
};
|
|
3702
|
+
const method = (req.method || "GET").toUpperCase();
|
|
3703
|
+
const deny = (code, msg) => {
|
|
3704
|
+
res.writeHead(code, { ...CORS, "Content-Type": "text/plain", "Cache-Control": "no-store" });
|
|
3705
|
+
res.end(msg);
|
|
3706
|
+
};
|
|
3707
|
+
if (method === "OPTIONS") {
|
|
3708
|
+
res.writeHead(204, CORS);
|
|
3709
|
+
res.end();
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const m = /^\/s\/([^/]+)(?:\/(.*))?$/.exec(basePath);
|
|
3713
|
+
if (!m) {
|
|
3714
|
+
deny(404, "Not found: expected /s/<sessionId>/<path>");
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
const sessionId = decodeURIComponent(m[1]);
|
|
3718
|
+
const relPath = m[2] ? `/${m[2]}` : "/";
|
|
3719
|
+
const target = this.sessionResolver ? this.sessionResolver(sessionId) : void 0;
|
|
3720
|
+
if (!target || !target.cwd) {
|
|
3721
|
+
deny(404, "Unknown session");
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
3725
|
+
if (!userEmail) {
|
|
3726
|
+
deny(401, "Authentication required");
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
const ownerEmail = (target.ownerEmail || "").toLowerCase();
|
|
3730
|
+
const isOwner = !!ownerEmail && userEmail.toLowerCase() === ownerEmail;
|
|
3731
|
+
if (!isOwner) {
|
|
3732
|
+
const sharing = target.sharing;
|
|
3733
|
+
if (!sharing || !sharing.enabled) {
|
|
3734
|
+
this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and session not shared`);
|
|
3735
|
+
deny(403, "Access denied");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
try {
|
|
3739
|
+
authorizeRequest({ user: { email: userEmail } }, sharing, "admin");
|
|
3740
|
+
} catch (err) {
|
|
3741
|
+
this.log(`Files-gateway DENY '${sessionId}': ${userEmail} \u2014 ${err?.message || "not admin"}`);
|
|
3742
|
+
deny(403, "Access denied");
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
const rootDir = target.cwd;
|
|
3747
|
+
if (method === "GET" || method === "HEAD") {
|
|
3748
|
+
if (url.searchParams.get("list") === "1") {
|
|
3749
|
+
const lex = containedPath(rootDir, relPath.replace(/^\/+/, ""));
|
|
3750
|
+
if (!lex) {
|
|
3751
|
+
deny(403, "Forbidden");
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
let entries;
|
|
3755
|
+
try {
|
|
3756
|
+
const dirents = fs.readdirSync(lex, { withFileTypes: true });
|
|
3757
|
+
entries = dirents.map((d) => {
|
|
3758
|
+
let size = 0, mtime = 0, isDir = d.isDirectory();
|
|
3759
|
+
try {
|
|
3760
|
+
const st = fs.statSync(path.join(lex, d.name));
|
|
3761
|
+
size = st.size;
|
|
3762
|
+
mtime = Math.floor(st.mtimeMs);
|
|
3763
|
+
isDir = st.isDirectory();
|
|
3764
|
+
} catch {
|
|
3765
|
+
}
|
|
3766
|
+
return { name: d.name, isDir, size, mtime };
|
|
3767
|
+
});
|
|
3768
|
+
} catch (err) {
|
|
3769
|
+
deny(err?.code === "ENOENT" ? 404 : 500, `Listing failed: ${err?.message || err}`);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
res.writeHead(200, { ...CORS, "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
3773
|
+
res.end(method === "HEAD" ? void 0 : JSON.stringify({ path: relPath, entries }));
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
serveStaticMount(req, res, { rootDir, relPath, mountUrlPrefix: "", browse: false});
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
if (method === "PUT" || method === "DELETE") {
|
|
3780
|
+
const filePath = this.resolveContainedPath(rootDir, relPath);
|
|
3781
|
+
if (!filePath) {
|
|
3782
|
+
deny(403, "Forbidden: path escapes session root");
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
if (method === "DELETE") {
|
|
3786
|
+
try {
|
|
3787
|
+
fs.unlinkSync(filePath);
|
|
3788
|
+
res.writeHead(204, CORS);
|
|
3789
|
+
res.end();
|
|
3790
|
+
} catch (err) {
|
|
3791
|
+
deny(err?.code === "ENOENT" ? 404 : 500, err?.message || "delete failed");
|
|
3792
|
+
}
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
let dir;
|
|
3796
|
+
try {
|
|
3797
|
+
dir = path.dirname(filePath);
|
|
3798
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3799
|
+
} catch (err) {
|
|
3800
|
+
deny(500, `mkdir failed: ${err?.message || err}`);
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
const tmp = path.join(dir, `.svamp-upload-${process.pid}-${crypto.randomBytes(6).toString("hex")}.part`);
|
|
3804
|
+
const ws = fs.createWriteStream(tmp);
|
|
3805
|
+
let settled = false;
|
|
3806
|
+
const cleanupPartial = () => {
|
|
3807
|
+
if (settled) return;
|
|
3808
|
+
settled = true;
|
|
3809
|
+
ws.destroy();
|
|
3810
|
+
fs.unlink(tmp, () => {
|
|
3811
|
+
});
|
|
3812
|
+
};
|
|
3813
|
+
req.on("aborted", cleanupPartial);
|
|
3814
|
+
req.on("close", () => {
|
|
3815
|
+
if (!ws.writableFinished && !req.complete) cleanupPartial();
|
|
3816
|
+
});
|
|
3817
|
+
ws.on("error", (err) => {
|
|
3818
|
+
if (settled) return;
|
|
3819
|
+
settled = true;
|
|
3820
|
+
fs.unlink(tmp, () => {
|
|
3821
|
+
});
|
|
3822
|
+
if (!res.headersSent) deny(500, err.message);
|
|
3823
|
+
});
|
|
3824
|
+
ws.on("finish", () => {
|
|
3825
|
+
if (settled) return;
|
|
3826
|
+
settled = true;
|
|
3827
|
+
try {
|
|
3828
|
+
fs.renameSync(tmp, filePath);
|
|
3829
|
+
res.writeHead(201, CORS);
|
|
3830
|
+
res.end();
|
|
3831
|
+
} catch (err) {
|
|
3832
|
+
fs.unlink(tmp, () => {
|
|
3833
|
+
});
|
|
3834
|
+
if (!res.headersSent) deny(500, `rename failed: ${err?.message || err}`);
|
|
3835
|
+
}
|
|
3836
|
+
});
|
|
3837
|
+
req.pipe(ws);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
deny(405, "Method not allowed");
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* #126: resolve a session-relative path against `rootDir` with lexical + symlink containment
|
|
3844
|
+
* (mirrors the static PUT handler's resolveContained). Allows a not-yet-existing target (for
|
|
3845
|
+
* PUT) as long as its deepest existing ancestor stays within the real root. Returns the
|
|
3846
|
+
* contained absolute path, or null on any traversal/symlink escape (fail-closed).
|
|
3847
|
+
*/
|
|
3848
|
+
resolveContainedPath(rootDir, relPath) {
|
|
3849
|
+
const base = path.resolve(rootDir);
|
|
3850
|
+
const lex = containedPath(base, relPath.replace(/^\/+/, ""));
|
|
3851
|
+
if (!lex) return null;
|
|
3852
|
+
try {
|
|
3853
|
+
const realRoot = fs.realpathSync(base);
|
|
3854
|
+
let ancestor = lex;
|
|
3855
|
+
while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor) {
|
|
3856
|
+
ancestor = path.dirname(ancestor);
|
|
3857
|
+
}
|
|
3858
|
+
const realAncestor = fs.realpathSync(ancestor);
|
|
3859
|
+
if (realAncestor !== realRoot && !realAncestor.startsWith(realRoot + path.sep)) return null;
|
|
3860
|
+
if (fs.existsSync(lex) && fs.lstatSync(lex).isSymbolicLink()) {
|
|
3861
|
+
const realTarget = fs.realpathSync(lex);
|
|
3862
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep)) return null;
|
|
3863
|
+
}
|
|
3864
|
+
} catch {
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
return lex;
|
|
3868
|
+
}
|
|
2757
3869
|
/** Start a lightweight Node.js HTTP proxy with auth + upload support. */
|
|
2758
3870
|
startAuthProxy() {
|
|
2759
3871
|
return new Promise((resolve, reject) => {
|
|
@@ -2798,6 +3910,10 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
2798
3910
|
res.end(html);
|
|
2799
3911
|
return;
|
|
2800
3912
|
}
|
|
3913
|
+
if (mount?.sessionFilesGateway) {
|
|
3914
|
+
await this.handleSessionFilesRequest(req, res, url, basePath);
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
2801
3917
|
if (mount && mount.access !== "public" && mount.access !== "link") {
|
|
2802
3918
|
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
2803
3919
|
if (mount.access === "owner" && !mount.ownerEmail) {
|
|
@@ -3153,7 +4269,7 @@ Connection: close\r
|
|
|
3153
4269
|
const mount = this.mounts.get(mountName);
|
|
3154
4270
|
const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
|
|
3155
4271
|
try {
|
|
3156
|
-
const { FrpcTunnel } = await
|
|
4272
|
+
const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
|
|
3157
4273
|
let tunnel;
|
|
3158
4274
|
tunnel = new FrpcTunnel({
|
|
3159
4275
|
name: tunnelName,
|
|
@@ -3249,7 +4365,11 @@ function writeEnvLines(lines) {
|
|
|
3249
4365
|
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
3250
4366
|
lines.pop();
|
|
3251
4367
|
}
|
|
3252
|
-
writeFileSync$1(file, lines.join("\n") + "\n", "utf-8");
|
|
4368
|
+
writeFileSync$1(file, lines.join("\n") + "\n", { encoding: "utf-8", mode: 384 });
|
|
4369
|
+
try {
|
|
4370
|
+
chmodSync$1(file, 384);
|
|
4371
|
+
} catch {
|
|
4372
|
+
}
|
|
3253
4373
|
}
|
|
3254
4374
|
function updateEnvFile(updates) {
|
|
3255
4375
|
const lines = readEnvLines();
|
|
@@ -3515,7 +4635,7 @@ function saveAccounts(accounts, svampHome) {
|
|
|
3515
4635
|
const payload = { version: 1, accounts };
|
|
3516
4636
|
writeFileSync$1(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
3517
4637
|
try {
|
|
3518
|
-
chmodSync(file, 384);
|
|
4638
|
+
chmodSync$1(file, 384);
|
|
3519
4639
|
} catch {
|
|
3520
4640
|
}
|
|
3521
4641
|
}
|
|
@@ -3570,7 +4690,7 @@ function upsertAccount(input, svampHome) {
|
|
|
3570
4690
|
const now = Date.now();
|
|
3571
4691
|
const existing = input.id ? accounts.find((a) => a.id === input.id) : void 0;
|
|
3572
4692
|
const account = {
|
|
3573
|
-
id: existing?.id || input.id || randomUUID(),
|
|
4693
|
+
id: existing?.id || input.id || randomUUID$1(),
|
|
3574
4694
|
label: input.label.trim(),
|
|
3575
4695
|
vendor,
|
|
3576
4696
|
method,
|
|
@@ -3829,7 +4949,7 @@ const ANTHROPIC_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callbac
|
|
|
3829
4949
|
const ANTHROPIC_SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
3830
4950
|
function generatePkce() {
|
|
3831
4951
|
const verifier = randomBytes(32).toString("base64url");
|
|
3832
|
-
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
4952
|
+
const challenge = createHash$1("sha256").update(verifier).digest("base64url");
|
|
3833
4953
|
return { verifier, challenge };
|
|
3834
4954
|
}
|
|
3835
4955
|
function buildAnthropicAuthUrl(challenge, state) {
|
|
@@ -3938,14 +5058,14 @@ function startAnthropicFlow(label) {
|
|
|
3938
5058
|
pruneFlows();
|
|
3939
5059
|
const { verifier, challenge } = generatePkce();
|
|
3940
5060
|
const state = randomBytes(32).toString("base64url");
|
|
3941
|
-
const flowId = randomUUID();
|
|
5061
|
+
const flowId = randomUUID$1();
|
|
3942
5062
|
const authorizeUrl = buildAnthropicAuthUrl(challenge, state);
|
|
3943
5063
|
flows.set(flowId, { id: flowId, vendor: "anthropic", label, createdAt: Date.now(), status: "pending", state, verifier });
|
|
3944
5064
|
return { flowId, authorizeUrl };
|
|
3945
5065
|
}
|
|
3946
5066
|
function startOpenAiDeviceFlow(label, log, svampHome) {
|
|
3947
5067
|
pruneFlows();
|
|
3948
|
-
const flowId = randomUUID();
|
|
5068
|
+
const flowId = randomUUID$1();
|
|
3949
5069
|
const codexHome = join$1(oauthFlowDir(flowId, svampHome), "codex");
|
|
3950
5070
|
mkdirSync$1(codexHome, { recursive: true });
|
|
3951
5071
|
const flow = { id: flowId, vendor: "openai", label, createdAt: Date.now(), status: "pending", codexHome, outputBuffer: "" };
|
|
@@ -4323,10 +5443,17 @@ function validateChannel(c) {
|
|
|
4323
5443
|
if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
|
|
4324
5444
|
errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
|
|
4325
5445
|
if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
|
|
4326
|
-
const MUTATING = ["run_bash", "send_to_session"];
|
|
4327
5446
|
const ag = c.action.agent || {};
|
|
4328
|
-
const
|
|
4329
|
-
|
|
5447
|
+
const offending = /* @__PURE__ */ new Set();
|
|
5448
|
+
for (const t of ag.tools || []) if (MUTATING_TOOLS.includes(t)) offending.add(t);
|
|
5449
|
+
for (const p of Object.values(ag.per_caller || {})) {
|
|
5450
|
+
for (const t of p?.tools || []) if (MUTATING_TOOLS.includes(t)) offending.add(t);
|
|
5451
|
+
}
|
|
5452
|
+
if (offending.size) {
|
|
5453
|
+
errs.push(
|
|
5454
|
+
`a caller-supplied agent channel without a shared_key may not grant mutating tools (${[...offending].sort().join(", ")})`
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
4330
5457
|
}
|
|
4331
5458
|
const unsafe = /[<>"'&\r\n]/;
|
|
4332
5459
|
if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
|
|
@@ -4503,11 +5630,15 @@ function generateSkillBody(channel, ctx) {
|
|
|
4503
5630
|
const res = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
|
|
4504
5631
|
if (res.status === "completed") return res.reply; // answered now \u2014 done
|
|
4505
5632
|
let cursor = 0; // else poll for the async reply
|
|
4506
|
-
|
|
5633
|
+
// BOUNDED: ~10 min (24 x wait:25). Do not loop forever \u2014 repliability is now the DEFAULT on
|
|
5634
|
+
// every ordinary message channel, so you may well be polling a notification channel nobody
|
|
5635
|
+
// is watching, and an unbounded loop would spin (and re-read the server-side outbox) forever.
|
|
5636
|
+
for (let i = 0; i < 24; i++) {
|
|
4507
5637
|
const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", correlationId: res.correlationId, cursor, wait: 25 });
|
|
4508
5638
|
cursor = r.cursor;
|
|
4509
5639
|
if (r.replies.length) return r.replies[0].body; // server matched it by correlationId
|
|
4510
5640
|
}
|
|
5641
|
+
return null; // no reply \u2014 this channel may be fire-and-forget
|
|
4511
5642
|
\`\`\`
|
|
4512
5643
|
**HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "correlationId": "<from send>", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).
|
|
4513
5644
|
> Tip: always pass the \`correlationId\` from \`send\` to \`receive\` \u2014 it is the reply key, so the reply finds you with no identity matching. Omit it and \`receive\` falls back to matching your \`from\`, which you must then supply identically on every call.
|
|
@@ -4987,6 +6118,7 @@ const SESSION_METHOD_MIN_ROLE = {
|
|
|
4987
6118
|
readFile: "admin",
|
|
4988
6119
|
writeFile: "admin",
|
|
4989
6120
|
writeFileChunk: "admin",
|
|
6121
|
+
filesEndpoint: "admin",
|
|
4990
6122
|
listDirectory: "admin",
|
|
4991
6123
|
bash: "admin",
|
|
4992
6124
|
issue: "admin",
|
|
@@ -5911,13 +7043,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5911
7043
|
if (session.outputBuffer.length > 1e3) {
|
|
5912
7044
|
session.outputBuffer.splice(0, session.outputBuffer.length - 500);
|
|
5913
7045
|
}
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
7046
|
+
if (terminalClient) {
|
|
7047
|
+
try {
|
|
7048
|
+
server.emit({
|
|
7049
|
+
type: "svamp:terminal-output",
|
|
7050
|
+
data: { type: "output", content: batch, sessionId },
|
|
7051
|
+
to: terminalClient
|
|
7052
|
+
});
|
|
7053
|
+
} catch {
|
|
7054
|
+
}
|
|
5921
7055
|
}
|
|
5922
7056
|
};
|
|
5923
7057
|
ptyProcess.onData((data) => {
|
|
@@ -5930,13 +7064,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5930
7064
|
session.exited = true;
|
|
5931
7065
|
session.exitCode = exitCode;
|
|
5932
7066
|
session.exitSignal = signal;
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
7067
|
+
if (terminalClient) {
|
|
7068
|
+
try {
|
|
7069
|
+
server.emit({
|
|
7070
|
+
type: "svamp:terminal-output",
|
|
7071
|
+
data: { type: "exit", content: "", sessionId, exitCode, signal },
|
|
7072
|
+
to: terminalClient
|
|
7073
|
+
});
|
|
7074
|
+
} catch {
|
|
7075
|
+
}
|
|
5940
7076
|
}
|
|
5941
7077
|
setTimeout(() => {
|
|
5942
7078
|
if (terminalSessions.has(sessionId)) {
|
|
@@ -6292,7 +7428,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6292
7428
|
const tunnels = handlers.tunnels;
|
|
6293
7429
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
6294
7430
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
6295
|
-
const { FrpcTunnel } = await
|
|
7431
|
+
const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
|
|
6296
7432
|
const tunnel = new FrpcTunnel({
|
|
6297
7433
|
name: params.name,
|
|
6298
7434
|
ports: params.ports,
|
|
@@ -6551,7 +7687,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6551
7687
|
const sessionRpc = handlers.getSessionRPCHandlers?.(sid);
|
|
6552
7688
|
if (sessionRpc?.saveChannel) {
|
|
6553
7689
|
try {
|
|
6554
|
-
const sharedKey = createHash("sha256").update(sid + (process.env.INTERNAL_API_SECRET || "")).digest("hex").slice(0, 32);
|
|
7690
|
+
const sharedKey = createHash$1("sha256").update(sid + (process.env.INTERNAL_API_SECRET || "")).digest("hex").slice(0, 32);
|
|
6555
7691
|
const meetingChannel = {
|
|
6556
7692
|
id: `meeting-${sid}`,
|
|
6557
7693
|
name: `Meeting WISE (${sid})`,
|
|
@@ -6785,7 +7921,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6785
7921
|
}
|
|
6786
7922
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
6787
7923
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
6788
|
-
const { toolsForRole } = await import('./sideband-
|
|
7924
|
+
const { toolsForRole } = await import('./sideband-C0CGbTIK.mjs');
|
|
6789
7925
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
6790
7926
|
return fmt(r2);
|
|
6791
7927
|
}
|
|
@@ -6884,13 +8020,13 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6884
8020
|
hyphaWorkspace: u?.scope?.current_workspace
|
|
6885
8021
|
});
|
|
6886
8022
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
6887
|
-
const callId = "call_" + randomUUID().slice(0, 10);
|
|
8023
|
+
const callId = "call_" + randomUUID$1().slice(0, 10);
|
|
6888
8024
|
const senderKey = (r.sender.name || "anonymous").toLowerCase();
|
|
6889
8025
|
if (!statelessLimiter.tryAcquire(senderKey)) {
|
|
6890
8026
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
6891
8027
|
}
|
|
6892
8028
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
6893
|
-
const { queryCore } = await import('./commands-
|
|
8029
|
+
const { queryCore } = await import('./commands-DRYk0mQr.mjs');
|
|
6894
8030
|
const timeout = c.reply?.timeout_sec || 120;
|
|
6895
8031
|
let result;
|
|
6896
8032
|
try {
|
|
@@ -7030,7 +8166,7 @@ ${d?.error || "not found"}`;
|
|
|
7030
8166
|
try {
|
|
7031
8167
|
const saved = writeUpload(dir, v.target, dec.data);
|
|
7032
8168
|
try {
|
|
7033
|
-
new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + randomUUID().slice(0, 10), outcome: "uploaded" });
|
|
8169
|
+
new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + randomUUID$1().slice(0, 10), outcome: "uploaded" });
|
|
7034
8170
|
} catch {
|
|
7035
8171
|
}
|
|
7036
8172
|
return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
|
|
@@ -7139,7 +8275,7 @@ function writeInboundContext(sessionId, ctx) {
|
|
|
7139
8275
|
try {
|
|
7140
8276
|
const p = ctxPath(sessionId);
|
|
7141
8277
|
mkdirSync$1(join$1(SVAMP_HOME$2, "inbound"), { recursive: true });
|
|
7142
|
-
const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
|
|
8278
|
+
const tmp = `${p}.${process.pid}.${randomUUID$1()}.tmp`;
|
|
7143
8279
|
writeFileSync$1(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
|
|
7144
8280
|
renameSync(tmp, p);
|
|
7145
8281
|
} catch {
|
|
@@ -7224,7 +8360,7 @@ function writeAwaiting(sessionId, map) {
|
|
|
7224
8360
|
try {
|
|
7225
8361
|
mkdirSync$1(join$1(SVAMP_HOME$2, "awaiting"), { recursive: true });
|
|
7226
8362
|
const p = awaitPath(sessionId);
|
|
7227
|
-
const tmp = `${p}.${process.pid}.${randomUUID()}.tmp`;
|
|
8363
|
+
const tmp = `${p}.${process.pid}.${randomUUID$1()}.tmp`;
|
|
7228
8364
|
writeFileSync$1(tmp, JSON.stringify(map));
|
|
7229
8365
|
renameSync(tmp, p);
|
|
7230
8366
|
} catch {
|
|
@@ -7486,7 +8622,7 @@ function ensureOutpostToken(projectDir, session) {
|
|
|
7486
8622
|
mkdirSync$1(dirname(file), { recursive: true });
|
|
7487
8623
|
writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
7488
8624
|
try {
|
|
7489
|
-
chmodSync(file, 384);
|
|
8625
|
+
chmodSync$1(file, 384);
|
|
7490
8626
|
} catch {
|
|
7491
8627
|
}
|
|
7492
8628
|
return rec;
|
|
@@ -7564,7 +8700,7 @@ function savePairings(pairings, svampHome) {
|
|
|
7564
8700
|
const payload = { version: 1, pairings };
|
|
7565
8701
|
writeFileSync$1(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
7566
8702
|
try {
|
|
7567
|
-
chmodSync(file, 384);
|
|
8703
|
+
chmodSync$1(file, 384);
|
|
7568
8704
|
} catch {
|
|
7569
8705
|
}
|
|
7570
8706
|
}
|
|
@@ -8034,14 +9170,35 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
|
8034
9170
|
return { messages: [], hasMore: false };
|
|
8035
9171
|
}
|
|
8036
9172
|
}
|
|
9173
|
+
const messageCountCache = /* @__PURE__ */ new Map();
|
|
8037
9174
|
function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
8038
9175
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
8039
9176
|
if (!existsSync(filePath)) return fallbackInMemory;
|
|
8040
9177
|
try {
|
|
8041
|
-
const
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
9178
|
+
const st = statSync(filePath);
|
|
9179
|
+
const cached = messageCountCache.get(filePath);
|
|
9180
|
+
if (cached && cached.size === st.size && cached.mtimeMs === st.mtimeMs) return cached.count;
|
|
9181
|
+
let count;
|
|
9182
|
+
if (cached && st.size > cached.size) {
|
|
9183
|
+
const fd = openSync(filePath, "r");
|
|
9184
|
+
try {
|
|
9185
|
+
const len = st.size - cached.size;
|
|
9186
|
+
const buf = Buffer.allocUnsafe(len);
|
|
9187
|
+
const read = readSync(fd, buf, 0, len, cached.size);
|
|
9188
|
+
let added = 0;
|
|
9189
|
+
for (let i = 0; i < read; i++) if (buf[i] === 10) added++;
|
|
9190
|
+
count = cached.count + added;
|
|
9191
|
+
if (read > 0 && buf[read - 1] !== 10) count++;
|
|
9192
|
+
} finally {
|
|
9193
|
+
closeSync(fd);
|
|
9194
|
+
}
|
|
9195
|
+
} else {
|
|
9196
|
+
const data = readFileSync(filePath, "utf-8");
|
|
9197
|
+
count = 0;
|
|
9198
|
+
for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
|
|
9199
|
+
if (data.length > 0 && data[data.length - 1] !== "\n") count++;
|
|
9200
|
+
}
|
|
9201
|
+
messageCountCache.set(filePath, { size: st.size, mtimeMs: st.mtimeMs, count });
|
|
8045
9202
|
return count;
|
|
8046
9203
|
} catch {
|
|
8047
9204
|
return fallbackInMemory;
|
|
@@ -8255,11 +9412,11 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8255
9412
|
if (role === "agent") {
|
|
8256
9413
|
const data = { ...content };
|
|
8257
9414
|
if ((data.type === "assistant" || data.type === "user") && !data.uuid) {
|
|
8258
|
-
data.uuid = randomUUID();
|
|
9415
|
+
data.uuid = randomUUID$1();
|
|
8259
9416
|
}
|
|
8260
9417
|
wrappedContent = { role: "agent", content: { type: "output", data } };
|
|
8261
9418
|
} else if (role === "event") {
|
|
8262
|
-
wrappedContent = { role: "agent", content: { type: "event", id: randomUUID(), data: content } };
|
|
9419
|
+
wrappedContent = { role: "agent", content: { type: "event", id: randomUUID$1(), data: content } };
|
|
8263
9420
|
} else if (role === "session") {
|
|
8264
9421
|
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
8265
9422
|
} else {
|
|
@@ -8267,7 +9424,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8267
9424
|
wrappedContent = { role: "user", content: { type: "text", text } };
|
|
8268
9425
|
}
|
|
8269
9426
|
const msg = {
|
|
8270
|
-
id: randomUUID(),
|
|
9427
|
+
id: randomUUID$1(),
|
|
8271
9428
|
seq: nextSeq++,
|
|
8272
9429
|
content: wrappedContent,
|
|
8273
9430
|
localId: null,
|
|
@@ -8305,7 +9462,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8305
9462
|
const msg = {
|
|
8306
9463
|
// #0816: CSPRNG — this id routes `svamp session inbox reply`, so a predictable
|
|
8307
9464
|
// one lets a third party target another agent's reply thread.
|
|
8308
|
-
messageId: "op_" + randomUUID().slice(0, 10),
|
|
9465
|
+
messageId: "op_" + randomUUID$1().slice(0, 10),
|
|
8309
9466
|
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"`,
|
|
8310
9467
|
timestamp: Date.now(),
|
|
8311
9468
|
read: false,
|
|
@@ -8550,10 +9707,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8550
9707
|
const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: parsed.content } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
|
|
8551
9708
|
if (isBtwEcho) wrappedContent.meta = { btw: true };
|
|
8552
9709
|
const msg = {
|
|
8553
|
-
id: randomUUID(),
|
|
9710
|
+
id: randomUUID$1(),
|
|
8554
9711
|
seq: nextSeq++,
|
|
8555
9712
|
content: wrappedContent,
|
|
8556
|
-
localId: effectiveLocalId || randomUUID(),
|
|
9713
|
+
localId: effectiveLocalId || randomUUID$1(),
|
|
8557
9714
|
createdAt: Date.now(),
|
|
8558
9715
|
updatedAt: Date.now()
|
|
8559
9716
|
};
|
|
@@ -8943,7 +10100,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8943
10100
|
hyphaWorkspace: u?.scope?.current_workspace
|
|
8944
10101
|
});
|
|
8945
10102
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
8946
|
-
const callId = "call_" + randomUUID().slice(0, 10);
|
|
10103
|
+
const callId = "call_" + randomUUID$1().slice(0, 10);
|
|
8947
10104
|
const ownerEmail = metadata.sharing?.owner;
|
|
8948
10105
|
const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
|
|
8949
10106
|
try {
|
|
@@ -9028,7 +10185,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9028
10185
|
if (dec.error || !dec.data) return { error: dec.error || "invalid content" };
|
|
9029
10186
|
const v = validateUpload({ projectDir: metadata.path, channel: c, filename: params.filename, bytes: dec.data.length, contentType: params.content_type });
|
|
9030
10187
|
if (v.error || !v.target) return { error: v.error || "upload rejected" };
|
|
9031
|
-
const callId = "call_" + randomUUID().slice(0, 10);
|
|
10188
|
+
const callId = "call_" + randomUUID$1().slice(0, 10);
|
|
9032
10189
|
try {
|
|
9033
10190
|
const saved = writeUpload(metadata.path, v.target, dec.data);
|
|
9034
10191
|
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "uploaded" });
|
|
@@ -9065,6 +10222,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9065
10222
|
channelReceive: async (params, context) => {
|
|
9066
10223
|
const c = channelStore.get(params.channel);
|
|
9067
10224
|
if (!c || c.enabled === false) return { error: "channel not found" };
|
|
10225
|
+
if (!channelAsyncRepliable(c)) {
|
|
10226
|
+
return { error: "this channel has no async replies (agent/loop/stateless channels reply synchronously)" };
|
|
10227
|
+
}
|
|
9068
10228
|
const u = context?.user;
|
|
9069
10229
|
const r = resolveSender(c, {
|
|
9070
10230
|
key: params.key,
|
|
@@ -9194,6 +10354,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9194
10354
|
await callbacks.onWriteFileChunk(path, content, uploadId, chunkIndex, totalChunks, isLast);
|
|
9195
10355
|
return { success: true };
|
|
9196
10356
|
},
|
|
10357
|
+
// #126: HTTP files-gateway base URL for this session (large upload/download over the frp
|
|
10358
|
+
// tunnel). Admin-gated like the other file ops. Returns { url: null } when the gateway tunnel
|
|
10359
|
+
// isn't up yet → the frontend falls back to the RPC chunked path.
|
|
10360
|
+
filesEndpoint: async (context) => {
|
|
10361
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
10362
|
+
return { url: callbacks.onFilesEndpoint?.() ?? null };
|
|
10363
|
+
},
|
|
9197
10364
|
listDirectory: async (path, context) => {
|
|
9198
10365
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
9199
10366
|
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
@@ -10741,10 +11908,10 @@ function computeCollectionConfigUpdate(existingConfig, extra) {
|
|
|
10741
11908
|
return { changed, merged: { ...cfg, ...extra } };
|
|
10742
11909
|
}
|
|
10743
11910
|
function emailHash(email) {
|
|
10744
|
-
return createHash("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
|
|
11911
|
+
return createHash$1("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
|
|
10745
11912
|
}
|
|
10746
11913
|
function sessionIdHash(sessionId) {
|
|
10747
|
-
return createHash("sha256").update(sessionId).digest("hex").slice(0, 12);
|
|
11914
|
+
return createHash$1("sha256").update(sessionId).digest("hex").slice(0, 12);
|
|
10748
11915
|
}
|
|
10749
11916
|
function roleLabel(role) {
|
|
10750
11917
|
switch (role) {
|
|
@@ -10864,7 +12031,7 @@ class SharingNotificationSync {
|
|
|
10864
12031
|
const shareType = params.shareType || "session";
|
|
10865
12032
|
const typeLabel = shareType === "machine" ? "Machine" : "Session";
|
|
10866
12033
|
await this.publishUserEvent({
|
|
10867
|
-
id: randomUUID(),
|
|
12034
|
+
id: randomUUID$1(),
|
|
10868
12035
|
recipientEmail: params.recipientEmail.toLowerCase(),
|
|
10869
12036
|
type: "sharing-invite",
|
|
10870
12037
|
source: {
|
|
@@ -10914,7 +12081,7 @@ class SharingNotificationSync {
|
|
|
10914
12081
|
} catch {
|
|
10915
12082
|
}
|
|
10916
12083
|
await this.publishUserEvent({
|
|
10917
|
-
id: randomUUID(),
|
|
12084
|
+
id: randomUUID$1(),
|
|
10918
12085
|
recipientEmail: recipientEmail.toLowerCase(),
|
|
10919
12086
|
type: "sharing-revoked",
|
|
10920
12087
|
source: {
|
|
@@ -11572,7 +12739,7 @@ class AcpBackend {
|
|
|
11572
12739
|
}
|
|
11573
12740
|
async startSession(initialPrompt) {
|
|
11574
12741
|
if (this.disposed) throw new Error("Backend has been disposed");
|
|
11575
|
-
const sessionId = randomUUID();
|
|
12742
|
+
const sessionId = randomUUID$1();
|
|
11576
12743
|
this.emit({ type: "status", status: "starting" });
|
|
11577
12744
|
let startupStatusErrorEmitted = false;
|
|
11578
12745
|
try {
|
|
@@ -11650,6 +12817,7 @@ class AcpBackend {
|
|
|
11650
12817
|
this.emit({ type: "status", status: "error", detail: err.message });
|
|
11651
12818
|
});
|
|
11652
12819
|
this.process.on("exit", (code, signal) => {
|
|
12820
|
+
this.process = null;
|
|
11653
12821
|
if (this.disposed) return;
|
|
11654
12822
|
if (code !== 0 && code !== null) {
|
|
11655
12823
|
signalStartupFailure(new Error(`Exit code: ${code}`));
|
|
@@ -11721,7 +12889,7 @@ class AcpBackend {
|
|
|
11721
12889
|
const extendedParams = params;
|
|
11722
12890
|
const toolCall = extendedParams.toolCall;
|
|
11723
12891
|
let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
|
|
11724
|
-
const toolCallId = toolCall?.id || randomUUID();
|
|
12892
|
+
const toolCallId = toolCall?.id || randomUUID$1();
|
|
11725
12893
|
const permissionId = toolCallId;
|
|
11726
12894
|
let input = {};
|
|
11727
12895
|
if (toolCall) {
|
|
@@ -12216,6 +13384,25 @@ var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
|
12216
13384
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
12217
13385
|
});
|
|
12218
13386
|
|
|
13387
|
+
function applyPermissionResolution(sessionService, requestId, approved) {
|
|
13388
|
+
const reqs = { ...sessionService._agentState?.requests };
|
|
13389
|
+
if (!(requestId in reqs)) return;
|
|
13390
|
+
const completedReqs = { ...sessionService._agentState?.completedRequests };
|
|
13391
|
+
const existingReq = reqs[requestId];
|
|
13392
|
+
delete reqs[requestId];
|
|
13393
|
+
sessionService.updateAgentState({
|
|
13394
|
+
controlledByUser: false,
|
|
13395
|
+
requests: reqs,
|
|
13396
|
+
completedRequests: {
|
|
13397
|
+
...completedReqs,
|
|
13398
|
+
[requestId]: {
|
|
13399
|
+
...existingReq || {},
|
|
13400
|
+
completedAt: Date.now(),
|
|
13401
|
+
status: approved ? "approved" : "denied"
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
});
|
|
13405
|
+
}
|
|
12219
13406
|
function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, log, onTurnEnd, getModelLabel) {
|
|
12220
13407
|
let pendingText = "";
|
|
12221
13408
|
let pendingThinking = "";
|
|
@@ -12325,22 +13512,7 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
12325
13512
|
break;
|
|
12326
13513
|
}
|
|
12327
13514
|
case "permission-response": {
|
|
12328
|
-
|
|
12329
|
-
const completedReqs = { ...sessionService._agentState?.completedRequests };
|
|
12330
|
-
const existingReq = reqs[msg.id];
|
|
12331
|
-
delete reqs[msg.id];
|
|
12332
|
-
sessionService.updateAgentState({
|
|
12333
|
-
controlledByUser: false,
|
|
12334
|
-
requests: reqs,
|
|
12335
|
-
completedRequests: {
|
|
12336
|
-
...completedReqs,
|
|
12337
|
-
[msg.id]: {
|
|
12338
|
-
...existingReq || {},
|
|
12339
|
-
completedAt: Date.now(),
|
|
12340
|
-
status: msg.approved ? "approved" : "denied"
|
|
12341
|
-
}
|
|
12342
|
-
}
|
|
12343
|
-
});
|
|
13515
|
+
applyPermissionResolution(sessionService, msg.id, msg.approved);
|
|
12344
13516
|
break;
|
|
12345
13517
|
}
|
|
12346
13518
|
case "event": {
|
|
@@ -12372,9 +13544,10 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
12372
13544
|
};
|
|
12373
13545
|
}
|
|
12374
13546
|
class HyphaPermissionHandler {
|
|
12375
|
-
constructor(shouldAutoAllow, log) {
|
|
13547
|
+
constructor(shouldAutoAllow, log, onAutoResolved) {
|
|
12376
13548
|
this.shouldAutoAllow = shouldAutoAllow;
|
|
12377
13549
|
this.log = log;
|
|
13550
|
+
this.onAutoResolved = onAutoResolved;
|
|
12378
13551
|
}
|
|
12379
13552
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
12380
13553
|
// #0445: safety auto-decline so an unanswered ACP/Gemini permission can't wedge the turn
|
|
@@ -12396,6 +13569,10 @@ class HyphaPermissionHandler {
|
|
|
12396
13569
|
this.pendingPermissions.delete(toolCallId);
|
|
12397
13570
|
this.log(`[ACP Permission] Auto-declining ${toolName} after ${timeoutMs}ms with no response`);
|
|
12398
13571
|
p.resolve({ decision: "denied" });
|
|
13572
|
+
try {
|
|
13573
|
+
this.onAutoResolved?.(toolCallId, false);
|
|
13574
|
+
} catch {
|
|
13575
|
+
}
|
|
12399
13576
|
}
|
|
12400
13577
|
}, timeoutMs);
|
|
12401
13578
|
timer.unref?.();
|
|
@@ -12422,6 +13599,10 @@ class HyphaPermissionHandler {
|
|
|
12422
13599
|
for (const [id, pending] of this.pendingPermissions) {
|
|
12423
13600
|
if (pending.timer) clearTimeout(pending.timer);
|
|
12424
13601
|
pending.resolve({ decision: "denied" });
|
|
13602
|
+
try {
|
|
13603
|
+
this.onAutoResolved?.(id, false);
|
|
13604
|
+
} catch {
|
|
13605
|
+
}
|
|
12425
13606
|
}
|
|
12426
13607
|
this.pendingPermissions.clear();
|
|
12427
13608
|
}
|
|
@@ -12502,7 +13683,7 @@ function codexCumulativeTokens(payload) {
|
|
|
12502
13683
|
return nonCachedInput + output + Math.round(cached * 0.1);
|
|
12503
13684
|
}
|
|
12504
13685
|
|
|
12505
|
-
const PINNED_CODEX_VERSION = "0.
|
|
13686
|
+
const PINNED_CODEX_VERSION = "0.147.0";
|
|
12506
13687
|
const MIN_CODEX_STEER_VERSION = { major: 0, minor: 129, patch: 0 };
|
|
12507
13688
|
function cmpVer(a, b) {
|
|
12508
13689
|
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
@@ -12605,6 +13786,7 @@ class CodexAppServerClient {
|
|
|
12605
13786
|
child.stderr?.on("data", (d) => this.log("[codex-app-server:stderr]", d.toString().trim()));
|
|
12606
13787
|
child.on("exit", (code) => {
|
|
12607
13788
|
const wasConnected = this.connected;
|
|
13789
|
+
if (this.process === child) this.process = null;
|
|
12608
13790
|
this.connected = false;
|
|
12609
13791
|
this.failAllPending(new Error(`codex app-server exited (code ${code})`));
|
|
12610
13792
|
this.resolvePendingTurn(true);
|
|
@@ -12948,7 +14130,7 @@ class CodexAppServerBackend {
|
|
|
12948
14130
|
}
|
|
12949
14131
|
listeners = [];
|
|
12950
14132
|
client;
|
|
12951
|
-
sessionId = randomUUID();
|
|
14133
|
+
sessionId = randomUUID$1();
|
|
12952
14134
|
log;
|
|
12953
14135
|
model;
|
|
12954
14136
|
// #0438: codex's own approval policy + sandbox, derived from the svamp permission mode. Mutable
|
|
@@ -13093,7 +14275,7 @@ class CodexAppServerBackend {
|
|
|
13093
14275
|
}
|
|
13094
14276
|
}
|
|
13095
14277
|
handleApproval(req) {
|
|
13096
|
-
const id = req.callId || randomUUID();
|
|
14278
|
+
const id = req.callId || randomUUID$1();
|
|
13097
14279
|
const reason = req.type === "exec" ? `Execute: ${(req.command ?? []).join(" ")}` : req.type === "patch" ? "Apply file changes" : req.toolName ? `Tool: ${req.toolName}` : "Codex requires approval";
|
|
13098
14280
|
const payload = req.type === "exec" ? { toolName: "CodexBash", input: { command: req.command, cwd: req.cwd } } : req.type === "patch" ? { toolName: "CodexPatch", input: { fileChanges: req.fileChanges } } : { toolName: req.toolName ?? "CodexTool", input: req.input };
|
|
13099
14281
|
this.emit({ type: "permission-request", id, reason, payload });
|
|
@@ -13139,7 +14321,7 @@ class CodexAppServerBackend {
|
|
|
13139
14321
|
break;
|
|
13140
14322
|
}
|
|
13141
14323
|
case "exec_command_begin": {
|
|
13142
|
-
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
14324
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
|
|
13143
14325
|
const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command ?? "");
|
|
13144
14326
|
this.emit({ type: "tool-call", toolName: "CodexBash", callId, args: { command, cwd: event.cwd } });
|
|
13145
14327
|
break;
|
|
@@ -13150,7 +14332,7 @@ class CodexAppServerBackend {
|
|
|
13150
14332
|
break;
|
|
13151
14333
|
}
|
|
13152
14334
|
case "patch_apply_begin": {
|
|
13153
|
-
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
14335
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
|
|
13154
14336
|
const { filePath, patch, kind } = summarizeCodexChanges(event.changes);
|
|
13155
14337
|
this.emit({ type: "tool-call", toolName: "CodexPatch", callId, args: {
|
|
13156
14338
|
filePath: filePath ?? event.file_path ?? event.filePath ?? null,
|
|
@@ -13170,7 +14352,7 @@ class CodexAppServerBackend {
|
|
|
13170
14352
|
break;
|
|
13171
14353
|
}
|
|
13172
14354
|
case "mcp_tool_call_begin": {
|
|
13173
|
-
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
14355
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID$1());
|
|
13174
14356
|
this.emit({ type: "tool-call", toolName: String(event.tool ?? "CodexMcpTool"), callId, args: { server: event.server, arguments: event.arguments } });
|
|
13175
14357
|
break;
|
|
13176
14358
|
}
|
|
@@ -13516,7 +14698,7 @@ function hasKimiProvider(cfg) {
|
|
|
13516
14698
|
}
|
|
13517
14699
|
function buildKimiCatalogAddArgs(cfg) {
|
|
13518
14700
|
if (!hasKimiProvider(cfg)) return null;
|
|
13519
|
-
const args = ["provider", "catalog", "add", cfg.provider
|
|
14701
|
+
const args = ["provider", "catalog", "add", cfg.provider];
|
|
13520
14702
|
if (cfg.apiBase) args.push("--base-url", cfg.apiBase);
|
|
13521
14703
|
const defaultModel = kimiDefaultModelId(cfg);
|
|
13522
14704
|
if (defaultModel) args.push("--default-model", defaultModel);
|
|
@@ -13532,6 +14714,19 @@ function kimiProviderConfigured(configTomlText, provider) {
|
|
|
13532
14714
|
const block = nextTable < 0 ? rest : rest.slice(0, nextTable);
|
|
13533
14715
|
return /(^|\n)\s*api_key\s*=/.test(block);
|
|
13534
14716
|
}
|
|
14717
|
+
function kimiModelMenu(env = process.env) {
|
|
14718
|
+
return (env.SVAMP_KIMI_MODELS || "").split(",").map((s) => s.trim()).filter(Boolean).filter((m, i, a) => a.indexOf(m) === i);
|
|
14719
|
+
}
|
|
14720
|
+
function kimiDefaultModel(env = process.env) {
|
|
14721
|
+
const menu = kimiModelMenu(env);
|
|
14722
|
+
return env.SVAMP_KIMI_MODEL || env.LLM_MODEL || menu[0] || void 0;
|
|
14723
|
+
}
|
|
14724
|
+
function kimiClampModel(requested, env = process.env) {
|
|
14725
|
+
const menu = kimiModelMenu(env);
|
|
14726
|
+
if (!menu.length) return requested;
|
|
14727
|
+
if (requested && menu.includes(requested)) return requested;
|
|
14728
|
+
return kimiDefaultModel(env);
|
|
14729
|
+
}
|
|
13535
14730
|
|
|
13536
14731
|
var kimiProvider = /*#__PURE__*/Object.freeze({
|
|
13537
14732
|
__proto__: null,
|
|
@@ -13539,7 +14734,10 @@ var kimiProvider = /*#__PURE__*/Object.freeze({
|
|
|
13539
14734
|
DEFAULT_KIMI_PROVIDER: DEFAULT_KIMI_PROVIDER,
|
|
13540
14735
|
buildKimiCatalogAddArgs: buildKimiCatalogAddArgs,
|
|
13541
14736
|
hasKimiProvider: hasKimiProvider,
|
|
14737
|
+
kimiClampModel: kimiClampModel,
|
|
14738
|
+
kimiDefaultModel: kimiDefaultModel,
|
|
13542
14739
|
kimiDefaultModelId: kimiDefaultModelId,
|
|
14740
|
+
kimiModelMenu: kimiModelMenu,
|
|
13543
14741
|
kimiProviderConfigured: kimiProviderConfigured,
|
|
13544
14742
|
kimiSpawnEnv: kimiSpawnEnv,
|
|
13545
14743
|
resolveKimiProvider: resolveKimiProvider
|
|
@@ -13553,7 +14751,7 @@ function readKimiConfig() {
|
|
|
13553
14751
|
return null;
|
|
13554
14752
|
}
|
|
13555
14753
|
}
|
|
13556
|
-
function ensureKimiProviderConfigured(env = process.env, command = "kimi", log = () => {
|
|
14754
|
+
async function ensureKimiProviderConfigured(env = process.env, command = "kimi", log = () => {
|
|
13557
14755
|
}) {
|
|
13558
14756
|
const cfg = resolveKimiProvider(env);
|
|
13559
14757
|
if (!hasKimiProvider(cfg)) return false;
|
|
@@ -13561,10 +14759,18 @@ function ensureKimiProviderConfigured(env = process.env, command = "kimi", log =
|
|
|
13561
14759
|
const args = buildKimiCatalogAddArgs(cfg);
|
|
13562
14760
|
if (!args) return false;
|
|
13563
14761
|
try {
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
14762
|
+
await new Promise((resolve, reject) => {
|
|
14763
|
+
execFile$1(
|
|
14764
|
+
command,
|
|
14765
|
+
args,
|
|
14766
|
+
{
|
|
14767
|
+
timeout: 3e4,
|
|
14768
|
+
// #0848: the API key travels in the child ENV, never on argv (argv is world-readable
|
|
14769
|
+
// via ps / /proc/<pid>/cmdline). kimi reads KIMI_REGISTRY_API_KEY when --api-key is omitted.
|
|
14770
|
+
env: { ...process.env, KIMI_CLI_NO_AUTO_UPDATE: "1", KIMI_REGISTRY_API_KEY: cfg.apiKey }
|
|
14771
|
+
},
|
|
14772
|
+
(err) => err ? reject(err) : resolve()
|
|
14773
|
+
);
|
|
13568
14774
|
});
|
|
13569
14775
|
log(`[kimi] configured provider '${cfg.provider}' in ${KIMI_CONFIG_PATH}`);
|
|
13570
14776
|
return kimiProviderConfigured(readKimiConfig(), cfg.provider);
|
|
@@ -14094,7 +15300,7 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
14094
15300
|
} catch {
|
|
14095
15301
|
}
|
|
14096
15302
|
}
|
|
14097
|
-
if (!credentialsCopied && platform() === "darwin") {
|
|
15303
|
+
if (!credentialsCopied && platform$1() === "darwin") {
|
|
14098
15304
|
const stagedCredFile = join$1(stagedClaudeDir, ".credentials.json");
|
|
14099
15305
|
const hasExistingCredentials = existsSync(stagedCredFile);
|
|
14100
15306
|
if (!hasExistingCredentials) {
|
|
@@ -14486,7 +15692,7 @@ class ProcessSupervisor {
|
|
|
14486
15692
|
}
|
|
14487
15693
|
const fullSpec = {
|
|
14488
15694
|
...spec,
|
|
14489
|
-
id: randomUUID
|
|
15695
|
+
id: randomUUID(),
|
|
14490
15696
|
createdAt: Date.now()
|
|
14491
15697
|
};
|
|
14492
15698
|
const entry = this.makeEntry(fullSpec);
|
|
@@ -15154,6 +16360,9 @@ function listIssues(projectRoot, opts = {}) {
|
|
|
15154
16360
|
if (opts.scope) items = items.filter((i) => i.scope === opts.scope);
|
|
15155
16361
|
return items.sort((a, b) => Number(a.id) - Number(b.id));
|
|
15156
16362
|
}
|
|
16363
|
+
function isPendingIssue(issue) {
|
|
16364
|
+
return issue.status !== "archived" && issue.status !== "paused";
|
|
16365
|
+
}
|
|
15157
16366
|
function isVisibleTo(issue, session) {
|
|
15158
16367
|
if (issue.scope === "session") return issue.session === session || issue.owner === session;
|
|
15159
16368
|
if (issue.owner === session) return true;
|
|
@@ -15246,7 +16455,7 @@ function _updateIssueUnlocked(projectRoot, id, patch) {
|
|
|
15246
16455
|
if (wasArchived !== nowArchived) {
|
|
15247
16456
|
const oldPath = issuePath(projectRoot, cur.id, wasArchived);
|
|
15248
16457
|
try {
|
|
15249
|
-
if (existsSync(oldPath)) unlinkSync(oldPath);
|
|
16458
|
+
if (existsSync(oldPath)) unlinkSync$1(oldPath);
|
|
15250
16459
|
} catch {
|
|
15251
16460
|
}
|
|
15252
16461
|
}
|
|
@@ -15323,6 +16532,47 @@ function summarize(issues) {
|
|
|
15323
16532
|
return { total: issues.length, ready: by("ready"), in_progress: by("in_progress"), paused: by("paused"), archived: by("archived") };
|
|
15324
16533
|
}
|
|
15325
16534
|
|
|
16535
|
+
const DEFAULT_LOG_PRUNE_POLICY = {
|
|
16536
|
+
keepFiles: 10,
|
|
16537
|
+
maxAgeDays: 14,
|
|
16538
|
+
maxBytes: 64 * 1024 * 1024
|
|
16539
|
+
// 64 MiB
|
|
16540
|
+
};
|
|
16541
|
+
const APPEND_ONLY_LOGS = ["daemon-supervised.log", "daemon-boot.log"];
|
|
16542
|
+
function isPerStartLog(name) {
|
|
16543
|
+
if (!name.startsWith("daemon-") || !name.endsWith(".log")) return false;
|
|
16544
|
+
return !APPEND_ONLY_LOGS.includes(name);
|
|
16545
|
+
}
|
|
16546
|
+
function planLogPrune(files, policy = DEFAULT_LOG_PRUNE_POLICY, nowMs = Date.now(), exclude = []) {
|
|
16547
|
+
const excluded = new Set(exclude);
|
|
16548
|
+
const keepFiles = Math.max(0, policy.keepFiles);
|
|
16549
|
+
const maxAgeMs = Math.max(0, policy.maxAgeDays) * 24 * 60 * 60 * 1e3;
|
|
16550
|
+
const perStart = files.filter((f) => isPerStartLog(f.name) && !excluded.has(f.name)).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
16551
|
+
const overCount = keepFiles > 0 ? perStart.slice(keepFiles) : perStart;
|
|
16552
|
+
const tooOld = maxAgeMs > 0 ? perStart.filter((f) => nowMs - f.mtimeMs > maxAgeMs) : [];
|
|
16553
|
+
const deleteFiles = [...new Set([...overCount, ...tooOld].map((f) => f.name))];
|
|
16554
|
+
const truncateFiles = files.filter((f) => APPEND_ONLY_LOGS.includes(f.name) && !excluded.has(f.name)).filter((f) => policy.maxBytes > 0 && f.size > policy.maxBytes).map((f) => ({ name: f.name, size: f.size }));
|
|
16555
|
+
return { deleteFiles, truncateFiles };
|
|
16556
|
+
}
|
|
16557
|
+
function resolveLogPrunePolicy(env = process.env) {
|
|
16558
|
+
const num = (raw, fallback) => {
|
|
16559
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
16560
|
+
const n = Number(raw);
|
|
16561
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
16562
|
+
};
|
|
16563
|
+
return {
|
|
16564
|
+
keepFiles: num(env.SVAMP_LOG_KEEP_FILES, DEFAULT_LOG_PRUNE_POLICY.keepFiles),
|
|
16565
|
+
maxAgeDays: num(env.SVAMP_LOG_MAX_AGE_DAYS, DEFAULT_LOG_PRUNE_POLICY.maxAgeDays),
|
|
16566
|
+
maxBytes: num(env.SVAMP_LOG_MAX_BYTES, DEFAULT_LOG_PRUNE_POLICY.maxBytes)
|
|
16567
|
+
};
|
|
16568
|
+
}
|
|
16569
|
+
function tailSlice(buf, tailBytes) {
|
|
16570
|
+
if (buf.length <= tailBytes) return buf;
|
|
16571
|
+
const cut = buf.subarray(buf.length - tailBytes);
|
|
16572
|
+
const nl = cut.indexOf(10);
|
|
16573
|
+
return nl >= 0 && nl + 1 < cut.length ? cut.subarray(nl + 1) : cut;
|
|
16574
|
+
}
|
|
16575
|
+
|
|
15326
16576
|
function isWorkflowEnabled(wf) {
|
|
15327
16577
|
return wf.enabled !== false;
|
|
15328
16578
|
}
|
|
@@ -15481,7 +16731,7 @@ function removeWorkflow(projectRoot, name) {
|
|
|
15481
16731
|
const p = workflowPath(projectRoot, name);
|
|
15482
16732
|
if (!existsSync(p)) return false;
|
|
15483
16733
|
try {
|
|
15484
|
-
unlinkSync(p);
|
|
16734
|
+
unlinkSync$1(p);
|
|
15485
16735
|
return true;
|
|
15486
16736
|
} catch {
|
|
15487
16737
|
return false;
|
|
@@ -15878,12 +17128,19 @@ function loopCancelledDuringVerify(current) {
|
|
|
15878
17128
|
}
|
|
15879
17129
|
function backlogOraclePending(projectRoot, sessionId) {
|
|
15880
17130
|
const inScope = (i) => !sessionId || isVisibleTo(i, sessionId);
|
|
15881
|
-
return listIssues(projectRoot).filter(inScope).filter(
|
|
17131
|
+
return listIssues(projectRoot).filter(inScope).filter(isPendingIssue).map((i) => ({ id: i.id }));
|
|
15882
17132
|
}
|
|
15883
17133
|
function isBacklogDrainerOracle(oracle) {
|
|
15884
17134
|
if (!oracle || typeof oracle !== "string") return false;
|
|
15885
17135
|
return /(^|\s)svamp\s+issue\s+pending(\s|$)/.test(oracle.trim());
|
|
15886
17136
|
}
|
|
17137
|
+
function oracleScopeSessionId(oracle, selfSessionId) {
|
|
17138
|
+
if (!oracle || typeof oracle !== "string") return void 0;
|
|
17139
|
+
const explicit = /(^|\s)--session[=\s]+(\S+)/.exec(oracle);
|
|
17140
|
+
if (explicit) return explicit[2];
|
|
17141
|
+
if (/(^|\s)--mine(\s|$)/.test(oracle)) return selfSessionId;
|
|
17142
|
+
return void 0;
|
|
17143
|
+
}
|
|
15887
17144
|
function resolveLoopDurable(cfg) {
|
|
15888
17145
|
if (typeof cfg.durable === "boolean") return cfg.durable;
|
|
15889
17146
|
return isBacklogDrainerOracle(cfg.oracle);
|
|
@@ -16490,7 +17747,7 @@ process.stdin.resume();
|
|
|
16490
17747
|
const cleanup = () => {
|
|
16491
17748
|
for (const p of cleanupPaths) {
|
|
16492
17749
|
try {
|
|
16493
|
-
if (existsSync(p)) unlinkSync(p);
|
|
17750
|
+
if (existsSync(p)) unlinkSync$1(p);
|
|
16494
17751
|
} catch {
|
|
16495
17752
|
}
|
|
16496
17753
|
}
|
|
@@ -16790,16 +18047,24 @@ async function installSkillFromMarketplace(name) {
|
|
|
16790
18047
|
const files = await collectFiles();
|
|
16791
18048
|
if (files.length === 0) throw new Error(`Skill ${name} has no files in marketplace`);
|
|
16792
18049
|
const targetDir = join(CLAUDE_SKILLS_DIR, name);
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
18050
|
+
const stagingDir = `${targetDir}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
18051
|
+
try {
|
|
18052
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
18053
|
+
for (const filePath of files) {
|
|
18054
|
+
const url = `${BASE}/files/${filePath}`;
|
|
18055
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
18056
|
+
if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
|
|
18057
|
+
const content = await resp.text();
|
|
18058
|
+
const localPath = join(stagingDir, filePath);
|
|
18059
|
+
if (!localPath.startsWith(stagingDir + "/")) continue;
|
|
18060
|
+
mkdirSync(dirname$1(localPath), { recursive: true });
|
|
18061
|
+
writeFileSync(localPath, content, "utf-8");
|
|
18062
|
+
}
|
|
18063
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
18064
|
+
renameSync$1(stagingDir, targetDir);
|
|
18065
|
+
} catch (err) {
|
|
18066
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
18067
|
+
throw err;
|
|
16803
18068
|
}
|
|
16804
18069
|
}
|
|
16805
18070
|
function getBundledSkillsDir() {
|
|
@@ -17128,7 +18393,7 @@ function readPendingRestart() {
|
|
|
17128
18393
|
}
|
|
17129
18394
|
function clearPendingRestart() {
|
|
17130
18395
|
try {
|
|
17131
|
-
if (existsSync$1(PENDING_RESTART_FILE)) unlinkSync
|
|
18396
|
+
if (existsSync$1(PENDING_RESTART_FILE)) unlinkSync(PENDING_RESTART_FILE);
|
|
17132
18397
|
} catch {
|
|
17133
18398
|
}
|
|
17134
18399
|
}
|
|
@@ -17141,7 +18406,7 @@ function writeStopMarker(reason) {
|
|
|
17141
18406
|
}
|
|
17142
18407
|
function clearStopMarker() {
|
|
17143
18408
|
try {
|
|
17144
|
-
if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync
|
|
18409
|
+
if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync(DAEMON_STOP_MARKER_FILE);
|
|
17145
18410
|
} catch {
|
|
17146
18411
|
}
|
|
17147
18412
|
}
|
|
@@ -17675,7 +18940,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
17675
18940
|
}
|
|
17676
18941
|
if (wasStopped) {
|
|
17677
18942
|
const eq = getMetadata().messageQueue || [];
|
|
17678
|
-
setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID
|
|
18943
|
+
setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop (extended)", createdAt: Date.now() }] }));
|
|
17679
18944
|
onLoopActivated?.();
|
|
17680
18945
|
}
|
|
17681
18946
|
sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop extended (+${grant.extension.grantedTurns} turns, ${grant.used}/${grant.max} used)${wasStopped ? " \u2014 resuming" : ""}. Reason: ${grant.extension.reason}`, level: "warning" }, "event");
|
|
@@ -17731,7 +18996,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
17731
18996
|
} catch {
|
|
17732
18997
|
}
|
|
17733
18998
|
const eq = getMetadata().messageQueue || [];
|
|
17734
|
-
setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID
|
|
18999
|
+
setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop", createdAt: Date.now() }] }));
|
|
17735
19000
|
onLoopActivated?.();
|
|
17736
19001
|
}
|
|
17737
19002
|
if (!wasStopped && (newMax != null || bumpedBudget)) {
|
|
@@ -17764,14 +19029,14 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
17764
19029
|
}
|
|
17765
19030
|
writeGoalLoopState(directory, sessionId, { active: true, phase: "running", durable, iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: hardMax, budget, started_at: Date.now(), holds: 0, ledger: { turns: 0, tokens: 0, window: [] }, progress_history: [], auto_resumes: 0, verify_errors: 0 });
|
|
17766
19031
|
const existingQueue = getMetadata().messageQueue || [];
|
|
17767
|
-
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID
|
|
19032
|
+
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
|
|
17768
19033
|
onLoopActivated?.();
|
|
17769
19034
|
sessionService.pushMessage({ type: "message", message: `\u{1F3AF} Goal set: ${condition.slice(0, 180)}${condition.length > 180 ? "\u2026" : ""}` }, "event");
|
|
17770
19035
|
logger.log(`[svampConfig] /goal set: "${condition.slice(0, 80)}"`);
|
|
17771
19036
|
} else {
|
|
17772
19037
|
writeGoalLoopState(directory, sessionId, { active: false, phase: "cancelled", completed_at: Date.now() });
|
|
17773
19038
|
const existingQueue = getMetadata().messageQueue || [];
|
|
17774
|
-
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID
|
|
19039
|
+
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID(), text: "/goal clear", displayText: "\u{1F3AF} Goal cleared", createdAt: Date.now() }] }));
|
|
17775
19040
|
onLoopActivated?.();
|
|
17776
19041
|
sessionService.pushMessage({ type: "message", message: "Goal cleared." }, "event");
|
|
17777
19042
|
logger.log(`[svampConfig] /goal cleared`);
|
|
@@ -17844,17 +19109,17 @@ function deletePersistedSession(sessionId) {
|
|
|
17844
19109
|
if (entry) {
|
|
17845
19110
|
const sessionFile = getSessionFilePath(entry.directory, sessionId);
|
|
17846
19111
|
try {
|
|
17847
|
-
if (existsSync$1(sessionFile)) unlinkSync
|
|
19112
|
+
if (existsSync$1(sessionFile)) unlinkSync(sessionFile);
|
|
17848
19113
|
} catch {
|
|
17849
19114
|
}
|
|
17850
19115
|
const messagesFile = getSessionMessagesPath(entry.directory, sessionId);
|
|
17851
19116
|
try {
|
|
17852
|
-
if (existsSync$1(messagesFile)) unlinkSync
|
|
19117
|
+
if (existsSync$1(messagesFile)) unlinkSync(messagesFile);
|
|
17853
19118
|
} catch {
|
|
17854
19119
|
}
|
|
17855
19120
|
const configFile = getSvampConfigPath(entry.directory, sessionId);
|
|
17856
19121
|
try {
|
|
17857
|
-
if (existsSync$1(configFile)) unlinkSync
|
|
19122
|
+
if (existsSync$1(configFile)) unlinkSync(configFile);
|
|
17858
19123
|
} catch {
|
|
17859
19124
|
}
|
|
17860
19125
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
@@ -17934,8 +19199,38 @@ function ensureHomeDir() {
|
|
|
17934
19199
|
mkdirSync(LOGS_DIR, { recursive: true });
|
|
17935
19200
|
}
|
|
17936
19201
|
}
|
|
19202
|
+
function pruneDaemonLogs() {
|
|
19203
|
+
try {
|
|
19204
|
+
const policy = resolveLogPrunePolicy(process.env);
|
|
19205
|
+
const entries = readdirSync$1(LOGS_DIR).map((name) => {
|
|
19206
|
+
try {
|
|
19207
|
+
const st = statSync$1(join(LOGS_DIR, name));
|
|
19208
|
+
return { name, size: st.size, mtimeMs: st.mtimeMs };
|
|
19209
|
+
} catch {
|
|
19210
|
+
return null;
|
|
19211
|
+
}
|
|
19212
|
+
}).filter((e) => e !== null);
|
|
19213
|
+
const plan = planLogPrune(entries, policy);
|
|
19214
|
+
for (const name of plan.deleteFiles) {
|
|
19215
|
+
try {
|
|
19216
|
+
unlinkSync(join(LOGS_DIR, name));
|
|
19217
|
+
} catch {
|
|
19218
|
+
}
|
|
19219
|
+
}
|
|
19220
|
+
for (const { name } of plan.truncateFiles) {
|
|
19221
|
+
try {
|
|
19222
|
+
const p = join(LOGS_DIR, name);
|
|
19223
|
+
const kept = tailSlice(readFileSync$1(p), Math.floor(policy.maxBytes / 8));
|
|
19224
|
+
writeFileSync(p, kept);
|
|
19225
|
+
} catch {
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
} catch {
|
|
19229
|
+
}
|
|
19230
|
+
}
|
|
17937
19231
|
function createLogger() {
|
|
17938
19232
|
ensureHomeDir();
|
|
19233
|
+
pruneDaemonLogs();
|
|
17939
19234
|
const logFile = join(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
|
|
17940
19235
|
return {
|
|
17941
19236
|
logFilePath: logFile,
|
|
@@ -18167,7 +19462,7 @@ async function startDaemon(options) {
|
|
|
18167
19462
|
machineId = readFileSync$1(machineIdFile, "utf-8").trim();
|
|
18168
19463
|
}
|
|
18169
19464
|
if (!machineId) {
|
|
18170
|
-
machineId = `machine-${os$1.hostname()}-${randomUUID
|
|
19465
|
+
machineId = `machine-${os$1.hostname()}-${randomUUID().slice(0, 8)}`;
|
|
18171
19466
|
try {
|
|
18172
19467
|
writeFileSync(machineIdFile, machineId, "utf-8");
|
|
18173
19468
|
} catch {
|
|
@@ -18215,7 +19510,7 @@ async function startDaemon(options) {
|
|
|
18215
19510
|
try {
|
|
18216
19511
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
18217
19512
|
if (!dir) return;
|
|
18218
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
19513
|
+
const { reconcileServiceLinks } = await import('./agentCommands-CI_sYhy6.mjs');
|
|
18219
19514
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
18220
19515
|
const config = readSvampConfig(configPath);
|
|
18221
19516
|
const entries = Array.from(urls.entries());
|
|
@@ -18233,7 +19528,7 @@ async function startDaemon(options) {
|
|
|
18233
19528
|
}
|
|
18234
19529
|
}
|
|
18235
19530
|
async function createExposedTunnel(spec) {
|
|
18236
|
-
const { FrpcTunnel } = await
|
|
19531
|
+
const { FrpcTunnel } = await Promise.resolve().then(function () { return frpc; });
|
|
18237
19532
|
const tunnel = new FrpcTunnel({
|
|
18238
19533
|
name: spec.name,
|
|
18239
19534
|
ports: spec.ports,
|
|
@@ -18261,7 +19556,7 @@ async function startDaemon(options) {
|
|
|
18261
19556
|
ensureAutoInstalledCommands(logger);
|
|
18262
19557
|
(async () => {
|
|
18263
19558
|
try {
|
|
18264
|
-
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-
|
|
19559
|
+
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-CnCpensV.mjs');
|
|
18265
19560
|
beginClaudeVersionReconcile((msg) => logger.log(msg));
|
|
18266
19561
|
} catch (e) {
|
|
18267
19562
|
logger.log(`[claude-version] check failed: ${e?.message || e}`);
|
|
@@ -18310,7 +19605,7 @@ async function startDaemon(options) {
|
|
|
18310
19605
|
logger.log(`[supervision] parent ${parentId.slice(0, 8)} not live \u2014 recording verdict on the backlog issue only (no in-memory wake)`);
|
|
18311
19606
|
}
|
|
18312
19607
|
if (send) Promise.resolve(send({
|
|
18313
|
-
messageId: randomUUID
|
|
19608
|
+
messageId: randomUUID(),
|
|
18314
19609
|
from: `agent:${v.sessionId}`,
|
|
18315
19610
|
fromSession: v.sessionId,
|
|
18316
19611
|
to: parentId,
|
|
@@ -18348,7 +19643,7 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
18348
19643
|
return;
|
|
18349
19644
|
}
|
|
18350
19645
|
Promise.resolve(send({
|
|
18351
|
-
messageId: randomUUID
|
|
19646
|
+
messageId: randomUUID(),
|
|
18352
19647
|
from: "system:issues",
|
|
18353
19648
|
subject: "issue follow-up",
|
|
18354
19649
|
body: text,
|
|
@@ -18916,7 +20211,7 @@ ${parts.join("\n")}`);
|
|
|
18916
20211
|
sessionMetadata = {
|
|
18917
20212
|
...sessionMetadata,
|
|
18918
20213
|
messageQueue: [...existingQueue, {
|
|
18919
|
-
id: randomUUID
|
|
20214
|
+
id: randomUUID(),
|
|
18920
20215
|
text,
|
|
18921
20216
|
displayText: "\u{1F501} Resuming loop",
|
|
18922
20217
|
createdAt: Date.now()
|
|
@@ -18928,7 +20223,7 @@ ${parts.join("\n")}`);
|
|
|
18928
20223
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
18929
20224
|
sessionMetadata = {
|
|
18930
20225
|
...sessionMetadata,
|
|
18931
|
-
messageQueue: [...existingQueue, { id: randomUUID
|
|
20226
|
+
messageQueue: [...existingQueue, { id: randomUUID(), text, displayText, createdAt: Date.now() }]
|
|
18932
20227
|
};
|
|
18933
20228
|
sessionService.updateMetadata(sessionMetadata);
|
|
18934
20229
|
};
|
|
@@ -20044,7 +21339,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20044
21339
|
// NOT re-push a display bubble (that second copy has localId:null and the
|
|
20045
21340
|
// frontend can't reconcile it → the message appears TWICE). Only re-send to
|
|
20046
21341
|
// the model. Inbox/loop items (no flag) still get their displayText push.
|
|
20047
|
-
messageQueue: [...existingQueue, { id: randomUUID
|
|
21342
|
+
messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
|
|
20048
21343
|
};
|
|
20049
21344
|
sessionService.updateMetadata(sessionMetadata);
|
|
20050
21345
|
signalProcessing(false);
|
|
@@ -20201,7 +21496,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20201
21496
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
20202
21497
|
sessionMetadata = {
|
|
20203
21498
|
...sessionMetadata,
|
|
20204
|
-
messageQueue: [...existingQueue, { id: randomUUID
|
|
21499
|
+
messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
|
|
20205
21500
|
};
|
|
20206
21501
|
sessionService.updateMetadata(sessionMetadata);
|
|
20207
21502
|
signalProcessing(true);
|
|
@@ -20578,11 +21873,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20578
21873
|
});
|
|
20579
21874
|
},
|
|
20580
21875
|
onIssue: async (params) => {
|
|
20581
|
-
const { issueRpc } = await import('./rpc-
|
|
21876
|
+
const { issueRpc } = await import('./rpc-CaKQ3aCj.mjs');
|
|
20582
21877
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
20583
21878
|
},
|
|
20584
21879
|
onWorkflow: async (params) => {
|
|
20585
|
-
const { workflowRpc } = await import('./rpc-
|
|
21880
|
+
const { workflowRpc } = await import('./rpc-DX7_LzZf.mjs');
|
|
20586
21881
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
20587
21882
|
},
|
|
20588
21883
|
onRipgrep: async (args, cwd) => {
|
|
@@ -20617,6 +21912,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20617
21912
|
onWriteFileChunk: async (path, content, uploadId, chunkIndex, totalChunks, isLast) => {
|
|
20618
21913
|
await writeSessionFileChunk(directory, sessionMetadata, path, content, uploadId, chunkIndex, isLast);
|
|
20619
21914
|
},
|
|
21915
|
+
onFilesEndpoint: () => serveManager$1.getSessionFilesEndpoint(sessionId),
|
|
20620
21916
|
onListDirectory: async (path) => {
|
|
20621
21917
|
const resolvedDir = resolve$1(directory, path || ".");
|
|
20622
21918
|
if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
|
|
@@ -20814,7 +22110,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20814
22110
|
enqueueLoopContinue();
|
|
20815
22111
|
processMessageQueueRef?.();
|
|
20816
22112
|
};
|
|
20817
|
-
pidToTrackedSession.set(randomUUID
|
|
22113
|
+
pidToTrackedSession.set(randomUUID(), trackedSession);
|
|
20818
22114
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
20819
22115
|
sessionService.updateMetadata(sessionMetadata);
|
|
20820
22116
|
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
@@ -20876,7 +22172,9 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20876
22172
|
if (!s || s.phase !== "dormant") return;
|
|
20877
22173
|
let pending = [];
|
|
20878
22174
|
try {
|
|
20879
|
-
|
|
22175
|
+
const root = resolveProjectRoot(directory);
|
|
22176
|
+
const scope = oracleScopeSessionId(typeof s.oracle === "string" ? s.oracle : void 0, sessionId);
|
|
22177
|
+
pending = backlogOraclePending(root, scope);
|
|
20880
22178
|
} catch {
|
|
20881
22179
|
return;
|
|
20882
22180
|
}
|
|
@@ -21002,7 +22300,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21002
22300
|
{
|
|
21003
22301
|
const forcedModel = agentName === "codex" ? codexForcedModel() : void 0;
|
|
21004
22302
|
const composerModel = typeof msgMeta?.model === "string" && msgMeta.model ? msgMeta.model : void 0;
|
|
21005
|
-
const rawModel = forcedModel || (agentName === "codex" && codexModelMenu().length ? codexClampModel(composerModel) : composerModel);
|
|
22303
|
+
const rawModel = forcedModel || (agentName === "codex" && codexModelMenu().length ? codexClampModel(composerModel) : void 0) || (agentName === "kimi" && kimiModelMenu().length ? kimiClampModel(composerModel) : void 0) || composerModel;
|
|
21006
22304
|
const desiredModel = rawModel || agentConfig?.default_model || void 0;
|
|
21007
22305
|
if (desiredModel !== appliedAgentModel) {
|
|
21008
22306
|
agentBackend?.setModel?.(desiredModel);
|
|
@@ -21017,7 +22315,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21017
22315
|
...sessionMetadata,
|
|
21018
22316
|
// #0411: alreadyStored — see Claude path. deliverUserMessageCore already stored
|
|
21019
22317
|
// this user message; the drain must not re-push a localId:null duplicate.
|
|
21020
|
-
messageQueue: [...existingQueue, { id: randomUUID
|
|
22318
|
+
messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
|
|
21021
22319
|
};
|
|
21022
22320
|
sessionService.updateMetadata(sessionMetadata);
|
|
21023
22321
|
return;
|
|
@@ -21029,7 +22327,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21029
22327
|
...sessionMetadata,
|
|
21030
22328
|
// #0411: alreadyStored — deliverUserMessageCore already stored this
|
|
21031
22329
|
// user message; the drain must not re-push a localId:null duplicate.
|
|
21032
|
-
messageQueue: [...existingQueue, { id: randomUUID
|
|
22330
|
+
messageQueue: [...existingQueue, { id: randomUUID(), text, createdAt: Date.now(), alreadyStored: true }]
|
|
21033
22331
|
};
|
|
21034
22332
|
sessionService.updateMetadata(sessionMetadata);
|
|
21035
22333
|
};
|
|
@@ -21286,11 +22584,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21286
22584
|
});
|
|
21287
22585
|
},
|
|
21288
22586
|
onIssue: async (params) => {
|
|
21289
|
-
const { issueRpc } = await import('./rpc-
|
|
22587
|
+
const { issueRpc } = await import('./rpc-CaKQ3aCj.mjs');
|
|
21290
22588
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
21291
22589
|
},
|
|
21292
22590
|
onWorkflow: async (params) => {
|
|
21293
|
-
const { workflowRpc } = await import('./rpc-
|
|
22591
|
+
const { workflowRpc } = await import('./rpc-DX7_LzZf.mjs');
|
|
21294
22592
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
21295
22593
|
},
|
|
21296
22594
|
onRipgrep: async (args, cwd) => {
|
|
@@ -21325,6 +22623,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21325
22623
|
onWriteFileChunk: async (path, content, uploadId, chunkIndex, totalChunks, isLast) => {
|
|
21326
22624
|
await writeSessionFileChunk(directory, sessionMetadata, path, content, uploadId, chunkIndex, isLast);
|
|
21327
22625
|
},
|
|
22626
|
+
onFilesEndpoint: () => serveManager$1.getSessionFilesEndpoint(sessionId),
|
|
21328
22627
|
onListDirectory: async (path) => {
|
|
21329
22628
|
const resolvedDir = resolve$1(directory, path || ".");
|
|
21330
22629
|
if (sessionMetadata.securityContext && resolvedDir !== resolve$1(directory) && !resolvedDir.startsWith(resolve$1(directory) + "/")) {
|
|
@@ -21424,7 +22723,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21424
22723
|
);
|
|
21425
22724
|
const checkSvampConfig = svampConfigChecker.check;
|
|
21426
22725
|
const writeSvampConfigPatchAcp = svampConfigChecker.writeConfig;
|
|
21427
|
-
const permissionHandler = new HyphaPermissionHandler(
|
|
22726
|
+
const permissionHandler = new HyphaPermissionHandler(
|
|
22727
|
+
shouldAutoAllow2,
|
|
22728
|
+
logger.log,
|
|
22729
|
+
(requestId, approved) => applyPermissionResolution(sessionService, requestId, approved)
|
|
22730
|
+
);
|
|
21428
22731
|
let agentIsoConfig;
|
|
21429
22732
|
const agentShouldIsolate = Boolean(options2.forceIsolation) || Boolean(options2.securityContext);
|
|
21430
22733
|
if (agentShouldIsolate && isolationCapabilities.preferred) {
|
|
@@ -21527,10 +22830,24 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
21527
22830
|
let kimiEnv = {};
|
|
21528
22831
|
if (agentName === "kimi") {
|
|
21529
22832
|
try {
|
|
21530
|
-
ensureKimiProviderConfigured(process.env, acpConfig.command, logger.log);
|
|
22833
|
+
await ensureKimiProviderConfigured(process.env, acpConfig.command, logger.log);
|
|
21531
22834
|
} catch {
|
|
21532
22835
|
}
|
|
21533
22836
|
kimiEnv = kimiSpawnEnv(process.env);
|
|
22837
|
+
const kimiMenu = kimiModelMenu();
|
|
22838
|
+
if (kimiMenu.length) {
|
|
22839
|
+
const kimiModel = kimiClampModel(options2.model ?? void 0) || kimiDefaultModel();
|
|
22840
|
+
sessionMetadata.models = kimiMenu.map((m) => ({
|
|
22841
|
+
code: m,
|
|
22842
|
+
value: m,
|
|
22843
|
+
description: m === kimiModel ? "Default on this machine" : "Available on this machine"
|
|
22844
|
+
}));
|
|
22845
|
+
try {
|
|
22846
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
22847
|
+
} catch {
|
|
22848
|
+
}
|
|
22849
|
+
logger.log(`[Agent Session ${sessionId}] Kimi model menu advertised (SVAMP_KIMI_MODELS): [${kimiMenu.join(", ")}] default=${kimiModel}`);
|
|
22850
|
+
}
|
|
21534
22851
|
}
|
|
21535
22852
|
agentBackend = new AcpBackend({
|
|
21536
22853
|
agentName,
|
|
@@ -21809,7 +23126,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21809
23126
|
});
|
|
21810
23127
|
}
|
|
21811
23128
|
};
|
|
21812
|
-
pidToTrackedSession.set(randomUUID
|
|
23129
|
+
pidToTrackedSession.set(randomUUID(), trackedSession);
|
|
21813
23130
|
let acpInboxReconcileTimer = null;
|
|
21814
23131
|
const reconcileAcpIdleInbox = () => {
|
|
21815
23132
|
if (acpStopped) {
|
|
@@ -21875,6 +23192,8 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21875
23192
|
"event"
|
|
21876
23193
|
);
|
|
21877
23194
|
sessionService.sendSessionEnd();
|
|
23195
|
+
agentBackend.dispose().catch(() => {
|
|
23196
|
+
});
|
|
21878
23197
|
if (!acpPersisted) {
|
|
21879
23198
|
deleteSession(sessionId);
|
|
21880
23199
|
} else {
|
|
@@ -22253,6 +23572,27 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
22253
23572
|
writeDaemonStateFile(localState);
|
|
22254
23573
|
logger.log("Daemon state file written \u2014 daemon is up; restoring mounts/sessions in background");
|
|
22255
23574
|
serveManager$1.restore().catch((err) => logger.error(`[serve] mount restore failed: ${err?.message || err}`));
|
|
23575
|
+
const daemonOwnerEmail = parseJwtEmail(process.env.HYPHA_TOKEN || "") || null;
|
|
23576
|
+
serveManager$1.setSessionResolver((sessionId) => {
|
|
23577
|
+
for (const [, session] of pidToTrackedSession) {
|
|
23578
|
+
if (session.svampSessionId === sessionId && !session.stopped && session.directory) {
|
|
23579
|
+
let sharing = null;
|
|
23580
|
+
try {
|
|
23581
|
+
sharing = session.hyphaService?.getMetadata?.()?.sharing ?? null;
|
|
23582
|
+
} catch {
|
|
23583
|
+
sharing = null;
|
|
23584
|
+
}
|
|
23585
|
+
return {
|
|
23586
|
+
cwd: session.directory,
|
|
23587
|
+
sharing,
|
|
23588
|
+
ownerEmail: sharing?.owner || daemonOwnerEmail
|
|
23589
|
+
};
|
|
23590
|
+
}
|
|
23591
|
+
}
|
|
23592
|
+
return null;
|
|
23593
|
+
});
|
|
23594
|
+
const sessionFilesToken = createHash("sha256").update(`session-files:${machineId}`).digest("hex").slice(0, 20);
|
|
23595
|
+
serveManager$1.ensureSessionFilesGateway(sessionFilesToken).then((base) => logger.log(`[serve] session-files gateway: ${base || "(tunnel pending)"}`)).catch((err) => logger.error(`[serve] session-files gateway failed: ${err?.message || err}`));
|
|
22256
23596
|
const persistedSessions = loadPersistedSessions();
|
|
22257
23597
|
try {
|
|
22258
23598
|
const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
|
|
@@ -22291,7 +23631,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
22291
23631
|
}
|
|
22292
23632
|
if (persistedSessions.length > 0) {
|
|
22293
23633
|
try {
|
|
22294
|
-
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-
|
|
23634
|
+
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-CnCpensV.mjs');
|
|
22295
23635
|
await awaitClaudeVersionReady();
|
|
22296
23636
|
} catch {
|
|
22297
23637
|
}
|
|
@@ -22536,7 +23876,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
22536
23876
|
const PING_TIMEOUT_MS = 15e3;
|
|
22537
23877
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
22538
23878
|
const RECONNECT_JITTER_MS = 2500;
|
|
22539
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
23879
|
+
const { WorkflowScheduler } = await import('./scheduler-B11LcOId.mjs');
|
|
22540
23880
|
const workflowProjectRoots = () => {
|
|
22541
23881
|
const dirs = /* @__PURE__ */ new Set();
|
|
22542
23882
|
for (const s of pidToTrackedSession.values()) {
|
|
@@ -23170,4 +24510,4 @@ var run = /*#__PURE__*/Object.freeze({
|
|
|
23170
24510
|
writeStopMarker: writeStopMarker
|
|
23171
24511
|
});
|
|
23172
24512
|
|
|
23173
|
-
export {
|
|
24513
|
+
export { listSkillFiles as $, saveWorkflow as A, rawWorkflow as B, listWorkflows as C, isWorkflowEnabled as D, workflowSchedules as E, inZone 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, kimiSetup as aA, api as aB, run as aC, computeOutboundHop as aa, registerAwaitingReply as ab, buildMachineShareUrl as ac, parseHandle as ad, handleMatchesMetadata as ae, PINNED_CODEX_VERSION as af, withFileLock as ag, describeMisconfiguration as ah, buildMachineDeps as ai, applyClaudeProxyEnv as aj, composeSessionId as ak, generateFriendlyName as al, generateHookSettings as am, instanceConfig as an, frpc as ao, staticFileServer as ap, claudeAuth as aq, codexProvider as ar, projectInfo as as, DefaultTransport$1 as at, acpBackend as au, acpAgentConfig as av, codexAppServerBackend as aw, GeminiTransport$1 as ax, KimiTransport$1 as ay, kimiProvider as az, stopDaemon as b, connectToHypha as c, daemonStatus as d, shortId as e, resolveProjectRoot as f, getHyphaServerUrl$1 as g, getIssue as h, resumeIssue as i, addComment as j, addIssue as k, listIssues as l, isPendingIssue as m, searchIssues as n, isVisibleTo as o, pauseIssue as p, getRun as q, registerMachineService as r, startDaemon as s, listRuns as t, updateIssue as u, getWorkflow as v, runWorkflow as w, setWorkflowEnabled as x, removeWorkflow as y, validateWorkflowName as z };
|