svamp-cli 0.2.305 → 0.2.307
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/dist/{adminCommands-DlD78CYk.mjs → adminCommands-DZ9MECkY.mjs} +1 -1
- package/dist/{agentCommands-B3FTfsg-.mjs → agentCommands-D68wBf-y.mjs} +5 -5
- package/dist/{auth-2-M7wMHL.mjs → auth-DDgr4YFE.mjs} +1 -1
- package/dist/{cli-CoQ_iSxW.mjs → cli-Bk6IPB9M.mjs} +76 -70
- package/dist/cli.mjs +2 -2
- package/dist/{commands-CfSAnB1T.mjs → commands-Bl9AtWkD.mjs} +2 -2
- package/dist/{commands-CjZKWU9a.mjs → commands-BlIR3KBq.mjs} +2 -2
- package/dist/{commands-HncLAND-.mjs → commands-CESl13XD.mjs} +2 -2
- package/dist/{commands--WPYI2sm.mjs → commands-CJVqMnNf.mjs} +1 -1
- package/dist/{commands-mm9jZg5L.mjs → commands-DJwSJOPD.mjs} +1 -1
- package/dist/{commands-CaeLSCaX.mjs → commands-Dh2S9Gf1.mjs} +1 -1
- package/dist/{commands-CG6AF59l.mjs → commands-DsfIIri_.mjs} +7 -7
- package/dist/{commands-BO-oHKFw.mjs → commands-rGFSTXKu.mjs} +2 -2
- package/dist/{fleet-GklB5DQn.mjs → fleet-okzhGLEY.mjs} +1 -1
- package/dist/{frpc-Dsada5ox.mjs → frpc-NDcBtpwa.mjs} +30 -6
- package/dist/{headlessCli-B2IWICNI.mjs → headlessCli-BkU5B5Jp.mjs} +2 -2
- package/dist/{httpServer-CSMZTpoh.mjs → httpServer-1XjB2h3K.mjs} +15 -2
- package/dist/index.mjs +1 -1
- package/dist/{notifyCommands-BzCPoBy8.mjs → notifyCommands-Oh0kvQgj.mjs} +1 -1
- package/dist/package-k1G6ygGP.mjs +64 -0
- package/dist/{rpc-DOVHyr_6.mjs → rpc-DSyBSuTF.mjs} +2 -1
- package/dist/{rpc--eK356Xz.mjs → rpc-K5xzddMF.mjs} +1 -1
- package/dist/{run-IqHyd8bA.mjs → run-09G0RL2B.mjs} +1 -1
- package/dist/{run-BtVKqoF4.mjs → run-DYVmYsvg.mjs} +861 -378
- package/dist/{scheduler-DAkzJVgr.mjs → scheduler-CtKlSyIq.mjs} +1 -1
- package/dist/{serveCommands-AEtGwfIU.mjs → serveCommands-BnOmV2Z3.mjs} +6 -6
- package/dist/{sideband-CSwHDjzp.mjs → sideband-CdJ0O6L6.mjs} +1 -1
- package/package.json +2 -2
- package/dist/package-DcJpavob.mjs +0 -64
|
@@ -8,8 +8,8 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { execFile, spawn, execSync, exec as exec$1 } from 'child_process';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
10
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
11
|
-
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
12
|
-
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync, rmSync as rmSync$1, cpSync, realpathSync, readdirSync,
|
|
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';
|
|
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';
|
|
@@ -19,7 +19,7 @@ import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:p
|
|
|
19
19
|
import { EventEmitter } from 'node:events';
|
|
20
20
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
21
21
|
import { createInterface } from 'node:readline';
|
|
22
|
-
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
|
|
22
|
+
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1, rename as rename$1 } from 'node:fs/promises';
|
|
23
23
|
import { promisify as promisify$1 } from 'node:util';
|
|
24
24
|
import { parse, stringify } from 'yaml';
|
|
25
25
|
|
|
@@ -1280,6 +1280,14 @@ const MIME = {
|
|
|
1280
1280
|
function contentTypeFor(filePath) {
|
|
1281
1281
|
return MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
1282
1282
|
}
|
|
1283
|
+
function hasBlockedDotSegment(relPath) {
|
|
1284
|
+
const decoded = safeDecode(relPath.replace(/^\/+/, ""));
|
|
1285
|
+
for (const seg of decoded.split("/")) {
|
|
1286
|
+
if (!seg || seg === "." || seg === "..") continue;
|
|
1287
|
+
if (seg.startsWith(".") && seg !== ".well-known") return true;
|
|
1288
|
+
}
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1283
1291
|
function containedPath(rootDir, relPath) {
|
|
1284
1292
|
const base = path.resolve(rootDir);
|
|
1285
1293
|
const decoded = safeDecode(relPath);
|
|
@@ -1368,6 +1376,11 @@ function serveStaticMount(req, res, opts) {
|
|
|
1368
1376
|
sendFile(req, res, rootDir, rootStat);
|
|
1369
1377
|
return;
|
|
1370
1378
|
}
|
|
1379
|
+
if (hasBlockedDotSegment(relPath)) {
|
|
1380
|
+
res.writeHead(404, CORS);
|
|
1381
|
+
res.end("Not Found");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1371
1384
|
const lexTarget = containedPath(rootDir, relPath.replace(/^\/+/, ""));
|
|
1372
1385
|
if (!lexTarget) {
|
|
1373
1386
|
res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
|
|
@@ -1496,6 +1509,7 @@ var staticFileServer = /*#__PURE__*/Object.freeze({
|
|
|
1496
1509
|
containedPath: containedPath,
|
|
1497
1510
|
contentTypeFor: contentTypeFor,
|
|
1498
1511
|
directoryListingHtml: directoryListingHtml,
|
|
1512
|
+
hasBlockedDotSegment: hasBlockedDotSegment,
|
|
1499
1513
|
parseRange: parseRange,
|
|
1500
1514
|
realContainedPath: realContainedPath,
|
|
1501
1515
|
serveStaticMount: serveStaticMount
|
|
@@ -1504,6 +1518,28 @@ var staticFileServer = /*#__PURE__*/Object.freeze({
|
|
|
1504
1518
|
const AUTH0_NAMESPACES = ["https://api.imjoy.io/", "https://amun.ai/"];
|
|
1505
1519
|
const COOKIE_NAME = "svamp_serve_token";
|
|
1506
1520
|
const SVAMP_SERVE_TOKEN_PARAM = "__svamp_serve_token";
|
|
1521
|
+
function stripServeAuthCookie(cookieHeader) {
|
|
1522
|
+
if (!cookieHeader) return cookieHeader;
|
|
1523
|
+
const kept = cookieHeader.split(";").map((s) => s.trim()).filter((s) => s && !s.toLowerCase().startsWith(COOKIE_NAME + "="));
|
|
1524
|
+
return kept.length ? kept.join("; ") : void 0;
|
|
1525
|
+
}
|
|
1526
|
+
function stripServeTokenParam(pathWithQuery) {
|
|
1527
|
+
const qIdx = pathWithQuery.indexOf("?");
|
|
1528
|
+
if (qIdx < 0) return pathWithQuery;
|
|
1529
|
+
const base = pathWithQuery.slice(0, qIdx);
|
|
1530
|
+
const params = new URLSearchParams(pathWithQuery.slice(qIdx + 1));
|
|
1531
|
+
params.delete(SVAMP_SERVE_TOKEN_PARAM);
|
|
1532
|
+
const qs = params.toString();
|
|
1533
|
+
return qs ? `${base}?${qs}` : base;
|
|
1534
|
+
}
|
|
1535
|
+
function sanitizeForwardHeaders(headers) {
|
|
1536
|
+
const out = { ...headers };
|
|
1537
|
+
const cleaned = stripServeAuthCookie(typeof out.cookie === "string" ? out.cookie : void 0);
|
|
1538
|
+
if (cleaned === void 0) delete out.cookie;
|
|
1539
|
+
else out.cookie = cleaned;
|
|
1540
|
+
delete out.authorization;
|
|
1541
|
+
return out;
|
|
1542
|
+
}
|
|
1507
1543
|
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1508
1544
|
const DEFAULT_CACHE_MAX_SIZE = 1e3;
|
|
1509
1545
|
function jsStringLiteral(value) {
|
|
@@ -2081,9 +2117,10 @@ function assertNonAdminMountDirSafe(directory, homeDir) {
|
|
|
2081
2117
|
}
|
|
2082
2118
|
function sanitizeMountForRole(mount, isAdmin) {
|
|
2083
2119
|
if (isAdmin) return mount;
|
|
2084
|
-
const { linkToken: _lt, directory: _dir, process: proc, ...rest } = mount;
|
|
2120
|
+
const { linkToken: _lt, directory: _dir, process: proc, url, ...rest } = mount;
|
|
2085
2121
|
const safeProcess = proc ? { ...proc, env: proc.env ? Object.fromEntries(Object.keys(proc.env).map((k) => [k, "***"])) : proc.env } : proc;
|
|
2086
|
-
|
|
2122
|
+
const safeUrl = mount.access === "link" ? void 0 : url;
|
|
2123
|
+
return { ...rest, process: safeProcess, ...safeUrl !== void 0 ? { url: safeUrl } : {} };
|
|
2087
2124
|
}
|
|
2088
2125
|
const DNS_LABEL_MAX = 63;
|
|
2089
2126
|
function buildLinkSubdomain(subdomainSafe, linkToken) {
|
|
@@ -2286,6 +2323,10 @@ class ServeManager {
|
|
|
2286
2323
|
}
|
|
2287
2324
|
return all;
|
|
2288
2325
|
}
|
|
2326
|
+
/** #0649: look up a single mount by name (for ownership checks before remove/replace). */
|
|
2327
|
+
getMount(name) {
|
|
2328
|
+
return this.mounts.get(name);
|
|
2329
|
+
}
|
|
2289
2330
|
/**
|
|
2290
2331
|
* Get server info — each mount has its own URL (per-mount subdomain).
|
|
2291
2332
|
*/
|
|
@@ -2437,9 +2478,9 @@ class ServeManager {
|
|
|
2437
2478
|
});
|
|
2438
2479
|
const warmupPath = cfg.warmupPath ?? "/";
|
|
2439
2480
|
const warmupTimeoutMs = cfg.warmupTimeoutMs ?? 3e4;
|
|
2440
|
-
const warmupPromise = this.warmupProbe(`http://127.0.0.1:${cfg.port}${warmupPath}`, warmupTimeoutMs).then(() => {
|
|
2481
|
+
const warmupPromise = this.warmupProbe(`http://127.0.0.1:${cfg.port}${warmupPath}`, warmupTimeoutMs, child).then(() => {
|
|
2441
2482
|
const h = this.managedProcs.get(name);
|
|
2442
|
-
if (h) {
|
|
2483
|
+
if (h && h.child === child) {
|
|
2443
2484
|
h.warmupPromise = null;
|
|
2444
2485
|
h.lastRequestAt = Date.now();
|
|
2445
2486
|
}
|
|
@@ -2447,7 +2488,8 @@ class ServeManager {
|
|
|
2447
2488
|
}).catch((err) => {
|
|
2448
2489
|
this.log(`Managed process '${name}' warmup failed: ${err.message}`);
|
|
2449
2490
|
this.killManagedTree(child, "SIGTERM");
|
|
2450
|
-
this.managedProcs.
|
|
2491
|
+
const h = this.managedProcs.get(name);
|
|
2492
|
+
if (h && h.child === child) this.managedProcs.delete(name);
|
|
2451
2493
|
throw err;
|
|
2452
2494
|
});
|
|
2453
2495
|
const newHandle = {
|
|
@@ -2482,10 +2524,13 @@ class ServeManager {
|
|
|
2482
2524
|
}
|
|
2483
2525
|
}
|
|
2484
2526
|
/** Poll a URL until it returns <500 or the deadline passes. */
|
|
2485
|
-
async warmupProbe(url, timeoutMs) {
|
|
2527
|
+
async warmupProbe(url, timeoutMs, child) {
|
|
2486
2528
|
const deadline = Date.now() + timeoutMs;
|
|
2487
2529
|
let lastErr;
|
|
2488
2530
|
while (Date.now() < deadline) {
|
|
2531
|
+
if (child && child.exitCode !== null) {
|
|
2532
|
+
throw new Error(`managed process exited (code=${child.exitCode}) before warmup completed`);
|
|
2533
|
+
}
|
|
2489
2534
|
try {
|
|
2490
2535
|
const ctrl = new AbortController();
|
|
2491
2536
|
const t = setTimeout(() => ctrl.abort(), 2e3);
|
|
@@ -2572,8 +2617,12 @@ class ServeManager {
|
|
|
2572
2617
|
};
|
|
2573
2618
|
try {
|
|
2574
2619
|
const tmp = `${this.persistFile}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
2575
|
-
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
2620
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
2576
2621
|
fs.renameSync(tmp, this.persistFile);
|
|
2622
|
+
try {
|
|
2623
|
+
fs.chmodSync(this.persistFile, 384);
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2577
2626
|
} catch (err) {
|
|
2578
2627
|
this.log(`Error persisting serve state: ${err.message}`);
|
|
2579
2628
|
}
|
|
@@ -2603,218 +2652,234 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
2603
2652
|
startAuthProxy() {
|
|
2604
2653
|
return new Promise((resolve, reject) => {
|
|
2605
2654
|
const server = http.createServer(async (req, res) => {
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
if (mount && mount.access !== "public" && mount.access !== "link") {
|
|
2646
|
-
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
2647
|
-
if (mount.access === "owner" && !mount.ownerEmail) {
|
|
2648
|
-
this.log(`Auth DENY '${mountName}': owner-only mount but ownerEmail is empty (misconfigured \u2014 the daemon's token has no email claim). Set access to an explicit email allowlist. requester=${userEmail || "anonymous"}`);
|
|
2649
|
-
const html = this.auth ? this.auth.getAccessDeniedHtml({ email: userEmail, access: mount.access, ownerEmail: mount.ownerEmail, misconfigured: true, returnUrl: req.url || "/" }) : "Access denied (misconfigured owner mount).";
|
|
2650
|
-
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2655
|
+
try {
|
|
2656
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
|
|
2657
|
+
const incomingHost = (req.headers.host || "").split(":")[0].toLowerCase();
|
|
2658
|
+
const hostMount = this.hostToMount.get(incomingHost);
|
|
2659
|
+
let mountName;
|
|
2660
|
+
let mountResolvedByHost = false;
|
|
2661
|
+
let basePath;
|
|
2662
|
+
if (hostMount && this.mounts.has(hostMount)) {
|
|
2663
|
+
mountName = hostMount;
|
|
2664
|
+
mountResolvedByHost = true;
|
|
2665
|
+
basePath = url.pathname;
|
|
2666
|
+
} else {
|
|
2667
|
+
mountName = url.pathname.split("/").filter(Boolean)[0];
|
|
2668
|
+
basePath = mountName ? url.pathname.slice(`/${mountName}`.length) || "/" : url.pathname;
|
|
2669
|
+
}
|
|
2670
|
+
const mount = mountName ? this.mounts.get(mountName) : void 0;
|
|
2671
|
+
if (basePath === "/__svamp_health" || url.pathname === "/__svamp_health") {
|
|
2672
|
+
res.writeHead(200, {
|
|
2673
|
+
"Content-Type": "application/json",
|
|
2674
|
+
"Cache-Control": "no-store"
|
|
2675
|
+
});
|
|
2676
|
+
res.end(JSON.stringify({
|
|
2677
|
+
ok: true,
|
|
2678
|
+
mount: mountName || null,
|
|
2679
|
+
ts: Date.now()
|
|
2680
|
+
}));
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (basePath === "/__login__" || url.pathname === "/__login__") {
|
|
2684
|
+
const returnUrl = url.searchParams.get("return") || "/";
|
|
2685
|
+
const isSameOriginPath = returnUrl.startsWith("/") && !returnUrl.startsWith("//") && !returnUrl.startsWith("/__login__");
|
|
2686
|
+
const safeReturn = isSameOriginPath ? returnUrl : "/";
|
|
2687
|
+
const html = this.auth ? this.auth.getLoginPageHtml(safeReturn) : "<h1>Auth not configured</h1>";
|
|
2688
|
+
res.writeHead(200, {
|
|
2689
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2690
|
+
"Cache-Control": "no-store"
|
|
2691
|
+
});
|
|
2651
2692
|
res.end(html);
|
|
2652
2693
|
return;
|
|
2653
2694
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
if (!
|
|
2657
|
-
this.log(`Auth
|
|
2658
|
-
const
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2695
|
+
if (mount && mount.access !== "public" && mount.access !== "link") {
|
|
2696
|
+
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
2697
|
+
if (mount.access === "owner" && !mount.ownerEmail) {
|
|
2698
|
+
this.log(`Auth DENY '${mountName}': owner-only mount but ownerEmail is empty (misconfigured \u2014 the daemon's token has no email claim). Set access to an explicit email allowlist. requester=${userEmail || "anonymous"}`);
|
|
2699
|
+
const html = this.auth ? this.auth.getAccessDeniedHtml({ email: userEmail, access: mount.access, ownerEmail: mount.ownerEmail, misconfigured: true, returnUrl: req.url || "/" }) : "Access denied (misconfigured owner mount).";
|
|
2700
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2701
|
+
res.end(html);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
const allowed = this.auth ? this.auth.isAuthorized(userEmail, mount.access, mount.ownerEmail) : false;
|
|
2705
|
+
if (!allowed) {
|
|
2706
|
+
if (!userEmail) {
|
|
2707
|
+
this.log(`Auth: '${mountName}' requires sign-in (no valid token) \u2014 redirecting to /__login__`);
|
|
2708
|
+
const loginUrl = `/__login__?return=${encodeURIComponent(req.url || "/")}`;
|
|
2709
|
+
const headers = { Location: loginUrl };
|
|
2710
|
+
if (hasCookieToken(req)) {
|
|
2711
|
+
headers["Set-Cookie"] = "svamp_serve_token=; Path=/; Max-Age=0; SameSite=Lax";
|
|
2712
|
+
}
|
|
2713
|
+
res.writeHead(302, headers);
|
|
2714
|
+
res.end();
|
|
2715
|
+
return;
|
|
2662
2716
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2717
|
+
const need = mount.access === "owner" ? `owner (${mount.ownerEmail || "unset"})` : `one of [${mount.access.join(", ")}]`;
|
|
2718
|
+
this.log(`Auth DENY '${mountName}': '${userEmail}' is not authorized (requires ${need})`);
|
|
2719
|
+
const html = this.auth ? this.auth.getAccessDeniedHtml({ email: userEmail, access: mount.access, ownerEmail: mount.ownerEmail, returnUrl: req.url || "/" }) : `Access denied for ${userEmail}.`;
|
|
2720
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2721
|
+
res.end(html);
|
|
2665
2722
|
return;
|
|
2666
2723
|
}
|
|
2667
|
-
|
|
2668
|
-
this.log(`Auth DENY '${mountName}': '${userEmail}' is not authorized (requires ${need})`);
|
|
2669
|
-
const html = this.auth ? this.auth.getAccessDeniedHtml({ email: userEmail, access: mount.access, ownerEmail: mount.ownerEmail, returnUrl: req.url || "/" }) : `Access denied for ${userEmail}.`;
|
|
2670
|
-
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2671
|
-
res.end(html);
|
|
2672
|
-
return;
|
|
2724
|
+
this.log(`Auth OK '${mountName}': ${userEmail}`);
|
|
2673
2725
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2726
|
+
const resolveContained = () => {
|
|
2727
|
+
const base = path.resolve(mount.directory);
|
|
2728
|
+
const fp = path.resolve(path.join(base, basePath));
|
|
2729
|
+
if (fp !== base && !fp.startsWith(base + path.sep)) return null;
|
|
2730
|
+
try {
|
|
2731
|
+
const realRoot = fs.realpathSync(base);
|
|
2732
|
+
let ancestor = fp;
|
|
2733
|
+
while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor) {
|
|
2734
|
+
ancestor = path.dirname(ancestor);
|
|
2735
|
+
}
|
|
2736
|
+
const realAncestor = fs.realpathSync(ancestor);
|
|
2737
|
+
if (realAncestor !== realRoot && !realAncestor.startsWith(realRoot + path.sep)) return null;
|
|
2738
|
+
if (fs.existsSync(fp) && fs.lstatSync(fp).isSymbolicLink()) {
|
|
2739
|
+
const realTarget = fs.realpathSync(fp);
|
|
2740
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep)) return null;
|
|
2741
|
+
}
|
|
2742
|
+
} catch {
|
|
2743
|
+
return null;
|
|
2685
2744
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2745
|
+
return fp;
|
|
2746
|
+
};
|
|
2747
|
+
if ((req.method === "PUT" || req.method === "DELETE") && mount && mount.directory) {
|
|
2748
|
+
if (mount.access === "public") {
|
|
2749
|
+
this.log(`Write DENY '${mountName}': ${req.method} not allowed on a public (unauthenticated) mount`);
|
|
2750
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2751
|
+
res.end("Write access denied: public mounts are read-only.");
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const filePath = resolveContained();
|
|
2755
|
+
if (!filePath) {
|
|
2756
|
+
this.log(`Write DENY '${mountName}': path escapes mount root (${basePath})`);
|
|
2757
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2758
|
+
res.end("Forbidden: path escapes mount root.");
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (req.method === "PUT") {
|
|
2762
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2763
|
+
const ws = fs.createWriteStream(filePath);
|
|
2764
|
+
let done = false;
|
|
2765
|
+
const cleanupPartial = () => {
|
|
2766
|
+
if (done) return;
|
|
2767
|
+
done = true;
|
|
2768
|
+
ws.destroy();
|
|
2769
|
+
fs.unlink(filePath, () => {
|
|
2770
|
+
});
|
|
2771
|
+
};
|
|
2772
|
+
req.on("aborted", cleanupPartial);
|
|
2773
|
+
req.on("close", () => {
|
|
2774
|
+
if (!ws.writableFinished && !req.complete) cleanupPartial();
|
|
2775
|
+
});
|
|
2776
|
+
req.pipe(ws);
|
|
2777
|
+
ws.on("finish", () => {
|
|
2778
|
+
done = true;
|
|
2779
|
+
res.writeHead(201);
|
|
2780
|
+
res.end();
|
|
2781
|
+
});
|
|
2782
|
+
ws.on("error", (err) => {
|
|
2783
|
+
if (done) return;
|
|
2784
|
+
done = true;
|
|
2785
|
+
res.writeHead(500);
|
|
2786
|
+
res.end(err.message);
|
|
2787
|
+
});
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
try {
|
|
2791
|
+
fs.unlinkSync(filePath);
|
|
2792
|
+
res.writeHead(204);
|
|
2793
|
+
res.end();
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
res.writeHead(err.code === "ENOENT" ? 404 : 500);
|
|
2796
|
+
res.end(err.message);
|
|
2691
2797
|
}
|
|
2692
|
-
} catch {
|
|
2693
|
-
return null;
|
|
2694
|
-
}
|
|
2695
|
-
return fp;
|
|
2696
|
-
};
|
|
2697
|
-
if ((req.method === "PUT" || req.method === "DELETE") && mount && mount.directory) {
|
|
2698
|
-
if (mount.access === "public") {
|
|
2699
|
-
this.log(`Write DENY '${mountName}': ${req.method} not allowed on a public (unauthenticated) mount`);
|
|
2700
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2701
|
-
res.end("Write access denied: public mounts are read-only.");
|
|
2702
|
-
return;
|
|
2703
|
-
}
|
|
2704
|
-
const filePath = resolveContained();
|
|
2705
|
-
if (!filePath) {
|
|
2706
|
-
this.log(`Write DENY '${mountName}': path escapes mount root (${basePath})`);
|
|
2707
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2708
|
-
res.end("Forbidden: path escapes mount root.");
|
|
2709
2798
|
return;
|
|
2710
2799
|
}
|
|
2711
|
-
if (
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2800
|
+
if (mount && mount.process) {
|
|
2801
|
+
const cfg = mount.process;
|
|
2802
|
+
if (cfg.wakeOnRequest || !this.managedProcs.has(mount.name)) {
|
|
2803
|
+
try {
|
|
2804
|
+
await this.ensureManagedRunning(mount.name);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
2807
|
+
res.end(`Backend not ready: ${err?.message || err}`);
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
const handle = this.managedProcs.get(mount.name);
|
|
2812
|
+
if (handle) {
|
|
2813
|
+
handle.lastRequestAt = Date.now();
|
|
2814
|
+
handle.activeConns++;
|
|
2815
|
+
let released = false;
|
|
2816
|
+
const release = () => {
|
|
2817
|
+
if (released) return;
|
|
2818
|
+
released = true;
|
|
2819
|
+
handle.activeConns = Math.max(0, handle.activeConns - 1);
|
|
2820
|
+
handle.lastRequestAt = Date.now();
|
|
2821
|
+
};
|
|
2822
|
+
res.on("close", release);
|
|
2823
|
+
}
|
|
2824
|
+
const targetPath = stripServeTokenParam(mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || ""));
|
|
2825
|
+
const proxyReq = http.request({
|
|
2826
|
+
hostname: "127.0.0.1",
|
|
2827
|
+
port: cfg.port,
|
|
2828
|
+
path: targetPath,
|
|
2829
|
+
method: req.method,
|
|
2830
|
+
headers: sanitizeForwardHeaders(req.headers)
|
|
2831
|
+
}, (proxyRes) => {
|
|
2832
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
2833
|
+
proxyRes.pipe(res);
|
|
2725
2834
|
});
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2835
|
+
proxyReq.on("error", (err) => {
|
|
2836
|
+
if (!res.headersSent) {
|
|
2837
|
+
res.writeHead(502);
|
|
2838
|
+
res.end(`Backend error: ${err.message}`);
|
|
2839
|
+
}
|
|
2731
2840
|
});
|
|
2732
|
-
|
|
2733
|
-
if (
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2841
|
+
const abortUpstream = () => {
|
|
2842
|
+
if (!proxyReq.destroyed) proxyReq.destroy();
|
|
2843
|
+
};
|
|
2844
|
+
req.on("aborted", abortUpstream);
|
|
2845
|
+
res.on("close", () => {
|
|
2846
|
+
if (!res.writableFinished) abortUpstream();
|
|
2737
2847
|
});
|
|
2848
|
+
proxyReq.setTimeout(12e4, abortUpstream);
|
|
2849
|
+
req.pipe(proxyReq);
|
|
2738
2850
|
return;
|
|
2739
2851
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2852
|
+
if (mount && mount.directory) {
|
|
2853
|
+
serveStaticMount(req, res, {
|
|
2854
|
+
rootDir: mount.directory,
|
|
2855
|
+
relPath: basePath || "/",
|
|
2856
|
+
// Path-based access is prefixed with /<mountName>; host-based has no prefix.
|
|
2857
|
+
mountUrlPrefix: mountResolvedByHost ? "" : `/${mountName}`,
|
|
2858
|
+
browse: true,
|
|
2859
|
+
log: (m) => this.log(m)
|
|
2860
|
+
});
|
|
2861
|
+
return;
|
|
2747
2862
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2863
|
+
const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*" };
|
|
2864
|
+
if (url.pathname === "/" || basePath === "/") {
|
|
2865
|
+
res.writeHead(200, { ...cors, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2866
|
+
res.end(this.mountListHtml());
|
|
2867
|
+
} else {
|
|
2868
|
+
res.writeHead(404, { ...cors, "Content-Type": "text/plain" });
|
|
2869
|
+
res.end("Not found");
|
|
2870
|
+
}
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
this.log(`Auth-proxy request error: ${err?.message || err}`);
|
|
2873
|
+
if (!res.headersSent) {
|
|
2753
2874
|
try {
|
|
2754
|
-
|
|
2755
|
-
} catch
|
|
2756
|
-
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
2757
|
-
res.end(`Backend not ready: ${err?.message || err}`);
|
|
2758
|
-
return;
|
|
2875
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2876
|
+
} catch {
|
|
2759
2877
|
}
|
|
2760
2878
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
handle.activeConns++;
|
|
2765
|
-
let released = false;
|
|
2766
|
-
const release = () => {
|
|
2767
|
-
if (released) return;
|
|
2768
|
-
released = true;
|
|
2769
|
-
handle.activeConns = Math.max(0, handle.activeConns - 1);
|
|
2770
|
-
handle.lastRequestAt = Date.now();
|
|
2771
|
-
};
|
|
2772
|
-
res.on("close", release);
|
|
2879
|
+
try {
|
|
2880
|
+
res.end("Internal Server Error");
|
|
2881
|
+
} catch {
|
|
2773
2882
|
}
|
|
2774
|
-
const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
|
|
2775
|
-
const proxyReq = http.request({
|
|
2776
|
-
hostname: "127.0.0.1",
|
|
2777
|
-
port: cfg.port,
|
|
2778
|
-
path: targetPath,
|
|
2779
|
-
method: req.method,
|
|
2780
|
-
headers: req.headers
|
|
2781
|
-
}, (proxyRes) => {
|
|
2782
|
-
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
2783
|
-
proxyRes.pipe(res);
|
|
2784
|
-
});
|
|
2785
|
-
proxyReq.on("error", (err) => {
|
|
2786
|
-
if (!res.headersSent) {
|
|
2787
|
-
res.writeHead(502);
|
|
2788
|
-
res.end(`Backend error: ${err.message}`);
|
|
2789
|
-
}
|
|
2790
|
-
});
|
|
2791
|
-
const abortUpstream = () => {
|
|
2792
|
-
if (!proxyReq.destroyed) proxyReq.destroy();
|
|
2793
|
-
};
|
|
2794
|
-
req.on("aborted", abortUpstream);
|
|
2795
|
-
res.on("close", () => {
|
|
2796
|
-
if (!res.writableFinished) abortUpstream();
|
|
2797
|
-
});
|
|
2798
|
-
proxyReq.setTimeout(12e4, abortUpstream);
|
|
2799
|
-
req.pipe(proxyReq);
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
if (mount && mount.directory) {
|
|
2803
|
-
serveStaticMount(req, res, {
|
|
2804
|
-
rootDir: mount.directory,
|
|
2805
|
-
relPath: basePath || "/",
|
|
2806
|
-
// Path-based access is prefixed with /<mountName>; host-based has no prefix.
|
|
2807
|
-
mountUrlPrefix: mountResolvedByHost ? "" : `/${mountName}`,
|
|
2808
|
-
browse: true});
|
|
2809
|
-
return;
|
|
2810
|
-
}
|
|
2811
|
-
const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*" };
|
|
2812
|
-
if (url.pathname === "/" || basePath === "/") {
|
|
2813
|
-
res.writeHead(200, { ...cors, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2814
|
-
res.end(this.mountListHtml());
|
|
2815
|
-
} else {
|
|
2816
|
-
res.writeHead(404, { ...cors, "Content-Type": "text/plain" });
|
|
2817
|
-
res.end("Not found");
|
|
2818
2883
|
}
|
|
2819
2884
|
});
|
|
2820
2885
|
server.on("upgrade", (req, clientSocket, head) => {
|
|
@@ -2882,11 +2947,20 @@ Connection: close\r
|
|
|
2882
2947
|
handle.lastRequestAt = Date.now();
|
|
2883
2948
|
});
|
|
2884
2949
|
}
|
|
2885
|
-
const targetPath = mountResolvedByHost ? req.url || "/" : (url.pathname.slice(`/${mountName}`.length) || "/") + (url.search || "");
|
|
2950
|
+
const targetPath = stripServeTokenParam(mountResolvedByHost ? req.url || "/" : (url.pathname.slice(`/${mountName}`.length) || "/") + (url.search || ""));
|
|
2886
2951
|
const upstream = net.connect(cfg.port, "127.0.0.1", () => {
|
|
2887
2952
|
const lines = [`${req.method} ${targetPath} HTTP/1.1`];
|
|
2888
2953
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
2889
|
-
|
|
2954
|
+
const name = req.rawHeaders[i];
|
|
2955
|
+
const lname = name.toLowerCase();
|
|
2956
|
+
let value = req.rawHeaders[i + 1];
|
|
2957
|
+
if (lname === "authorization") continue;
|
|
2958
|
+
if (lname === "cookie") {
|
|
2959
|
+
const cleaned = stripServeAuthCookie(value);
|
|
2960
|
+
if (cleaned === void 0) continue;
|
|
2961
|
+
value = cleaned;
|
|
2962
|
+
}
|
|
2963
|
+
lines.push(`${name}: ${value}`);
|
|
2890
2964
|
}
|
|
2891
2965
|
upstream.write(lines.join("\r\n") + "\r\n\r\n");
|
|
2892
2966
|
if (head && head.length) upstream.write(head);
|
|
@@ -2973,7 +3047,7 @@ Connection: close\r
|
|
|
2973
3047
|
const mount = this.mounts.get(mountName);
|
|
2974
3048
|
const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
|
|
2975
3049
|
try {
|
|
2976
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
3050
|
+
const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
|
|
2977
3051
|
let tunnel;
|
|
2978
3052
|
tunnel = new FrpcTunnel({
|
|
2979
3053
|
name: tunnelName,
|
|
@@ -3910,6 +3984,46 @@ function renderTemplate(template, ctx) {
|
|
|
3910
3984
|
});
|
|
3911
3985
|
}
|
|
3912
3986
|
|
|
3987
|
+
const _sleepSab$1 = new Int32Array(new SharedArrayBuffer(4));
|
|
3988
|
+
function sleepSync$1(ms) {
|
|
3989
|
+
try {
|
|
3990
|
+
Atomics.wait(_sleepSab$1, 0, 0, ms);
|
|
3991
|
+
} catch {
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
function withFileLock(lockPath, fn, opts) {
|
|
3995
|
+
const deadlineMs = 50;
|
|
3996
|
+
const staleMs = 5e3;
|
|
3997
|
+
const deadline = Date.now() + deadlineMs;
|
|
3998
|
+
let held = false;
|
|
3999
|
+
while (Date.now() < deadline) {
|
|
4000
|
+
try {
|
|
4001
|
+
mkdirSync$1(lockPath);
|
|
4002
|
+
held = true;
|
|
4003
|
+
break;
|
|
4004
|
+
} catch {
|
|
4005
|
+
try {
|
|
4006
|
+
if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
|
|
4007
|
+
rmSync$1(lockPath, { recursive: true, force: true });
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
} catch {
|
|
4011
|
+
}
|
|
4012
|
+
sleepSync$1(5);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
try {
|
|
4016
|
+
return fn();
|
|
4017
|
+
} finally {
|
|
4018
|
+
if (held) {
|
|
4019
|
+
try {
|
|
4020
|
+
rmSync$1(lockPath, { recursive: true, force: true });
|
|
4021
|
+
} catch {
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
|
|
3913
4027
|
const HARD_MAX_BYTES = 25 * 1024 * 1024;
|
|
3914
4028
|
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
3915
4029
|
const DEFAULT_MAX_COUNT = 100;
|
|
@@ -4125,6 +4239,22 @@ class ChannelStore {
|
|
|
4125
4239
|
_path(id) {
|
|
4126
4240
|
return join$1(this.dir, `${id}.json`);
|
|
4127
4241
|
}
|
|
4242
|
+
_lock(id) {
|
|
4243
|
+
return join$1(this.dir, `${id}.json.lock`);
|
|
4244
|
+
}
|
|
4245
|
+
// #0679: the actual validate + atomic write, WITHOUT the lock, so a locked RMW mutator can
|
|
4246
|
+
// reuse it inside its own lock section (no re-entrant re-lock / deadlock).
|
|
4247
|
+
_writeChannel(channel) {
|
|
4248
|
+
const c = { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
|
|
4249
|
+
if (!c.id) c.id = genId();
|
|
4250
|
+
const errs = validateChannel(c);
|
|
4251
|
+
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
4252
|
+
mkdirSync$1(this.dir, { recursive: true });
|
|
4253
|
+
const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
4254
|
+
writeFileSync$1(tmp, JSON.stringify(c, null, 2));
|
|
4255
|
+
renameSync(tmp, this._path(c.id));
|
|
4256
|
+
return c;
|
|
4257
|
+
}
|
|
4128
4258
|
list() {
|
|
4129
4259
|
if (!existsSync(this.dir)) return [];
|
|
4130
4260
|
return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
@@ -4143,15 +4273,8 @@ class ChannelStore {
|
|
|
4143
4273
|
}
|
|
4144
4274
|
}
|
|
4145
4275
|
save(channel) {
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
const errs = validateChannel(c);
|
|
4149
|
-
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
4150
|
-
mkdirSync$1(this.dir, { recursive: true });
|
|
4151
|
-
const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
4152
|
-
writeFileSync$1(tmp, JSON.stringify(c, null, 2));
|
|
4153
|
-
renameSync(tmp, this._path(c.id));
|
|
4154
|
-
return c;
|
|
4276
|
+
if (channel.id) return withFileLock(this._lock(channel.id), () => this._writeChannel(channel));
|
|
4277
|
+
return this._writeChannel(channel);
|
|
4155
4278
|
}
|
|
4156
4279
|
remove(id) {
|
|
4157
4280
|
const p = this._path(id);
|
|
@@ -4161,28 +4284,40 @@ class ChannelStore {
|
|
|
4161
4284
|
}
|
|
4162
4285
|
return false;
|
|
4163
4286
|
}
|
|
4287
|
+
// #0679: setEnabled/recordCall/addCaller are read-modify-write mutators — multiple ChannelStore
|
|
4288
|
+
// instances (one per session) point at the same .svamp/channels/<id>.json, so without
|
|
4289
|
+
// serialization two concurrent RMWs both read the same base and the second save() clobbers the
|
|
4290
|
+
// first (addCaller silently DROPS a freshly-generated caller key). Hold the bounded fail-open
|
|
4291
|
+
// per-channel lock across the whole read→mutate→write so the read sees the other's committed
|
|
4292
|
+
// change (mirrors inboxGuard's #0625 withAwaitLock). _writeChannel is the un-locked write.
|
|
4164
4293
|
setEnabled(id, enabled) {
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4294
|
+
return withFileLock(this._lock(id), () => {
|
|
4295
|
+
const c = this.get(id);
|
|
4296
|
+
if (!c) return null;
|
|
4297
|
+
c.enabled = enabled;
|
|
4298
|
+
return this._writeChannel(c);
|
|
4299
|
+
});
|
|
4169
4300
|
}
|
|
4170
4301
|
recordCall(id, entry) {
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4302
|
+
withFileLock(this._lock(id), () => {
|
|
4303
|
+
const c = this.get(id);
|
|
4304
|
+
if (!c) return;
|
|
4305
|
+
c.last_calls = c.last_calls || [];
|
|
4306
|
+
c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4307
|
+
c.last_calls = c.last_calls.slice(0, 20);
|
|
4308
|
+
this._writeChannel(c);
|
|
4309
|
+
});
|
|
4177
4310
|
}
|
|
4178
4311
|
addCaller(id, name, kind = "agent") {
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4312
|
+
return withFileLock(this._lock(id), () => {
|
|
4313
|
+
const c = this.get(id);
|
|
4314
|
+
if (!c) return null;
|
|
4315
|
+
c.identity.callers = c.identity.callers || [];
|
|
4316
|
+
const caller = { name, kind, key: genKey() };
|
|
4317
|
+
c.identity.callers.push(caller);
|
|
4318
|
+
this._writeChannel(c);
|
|
4319
|
+
return caller;
|
|
4320
|
+
});
|
|
4186
4321
|
}
|
|
4187
4322
|
}
|
|
4188
4323
|
function routingSession(channel, ctx) {
|
|
@@ -4326,6 +4461,13 @@ curl -X POST "${sendUrl}" \\
|
|
|
4326
4461
|
Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${queueSection}`;
|
|
4327
4462
|
}
|
|
4328
4463
|
|
|
4464
|
+
function keysMatch(a, b) {
|
|
4465
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
4466
|
+
const ba = Buffer.from(a);
|
|
4467
|
+
const bb = Buffer.from(b);
|
|
4468
|
+
if (ba.length !== bb.length) return false;
|
|
4469
|
+
return timingSafeEqual(ba, bb);
|
|
4470
|
+
}
|
|
4329
4471
|
function resolveSender(channel, input = {}) {
|
|
4330
4472
|
const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
|
|
4331
4473
|
const id = channel.identity || {};
|
|
@@ -4342,7 +4484,7 @@ function resolveSender(channel, input = {}) {
|
|
|
4342
4484
|
return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
|
|
4343
4485
|
}
|
|
4344
4486
|
if (id.mode === "per-key") {
|
|
4345
|
-
const caller = (id.callers || []).find((c) => c.key && c.key
|
|
4487
|
+
const caller = (id.callers || []).find((c) => c.key && keysMatch(c.key, key));
|
|
4346
4488
|
if (!caller) {
|
|
4347
4489
|
const acceptsHypha = Array.isArray(id.hypha_allow) && id.hypha_allow.length > 0;
|
|
4348
4490
|
return { error: acceptsHypha ? "invalid or missing key \u2014 supply a caller key issued by the channel owner, or call with an authenticated Hypha identity (this channel accepts hypha_allow callers; anonymous/keyless requests are rejected)" : "invalid or missing key \u2014 this channel requires a caller key issued by the channel owner" };
|
|
@@ -4350,7 +4492,7 @@ function resolveSender(channel, input = {}) {
|
|
|
4350
4492
|
return { sender: { name: caller.name, kind: caller.kind, verified: true } };
|
|
4351
4493
|
}
|
|
4352
4494
|
if (id.mode === "caller-supplied") {
|
|
4353
|
-
if (id.shared_key && key
|
|
4495
|
+
if (id.shared_key && !keysMatch(key, id.shared_key)) return { error: "invalid key" };
|
|
4354
4496
|
return { sender: { name: from || "anonymous", kind: "user", verified: false } };
|
|
4355
4497
|
}
|
|
4356
4498
|
return { error: "unsupported identity mode" };
|
|
@@ -4592,6 +4734,11 @@ class ChannelOutbox {
|
|
|
4592
4734
|
this.reload();
|
|
4593
4735
|
this._compact();
|
|
4594
4736
|
}
|
|
4737
|
+
{
|
|
4738
|
+
const sig = this._stat();
|
|
4739
|
+
this._lastMtimeMs = sig.mtimeMs;
|
|
4740
|
+
this._lastSize = sig.size;
|
|
4741
|
+
}
|
|
4595
4742
|
this.emitter.emit(channelId, reply);
|
|
4596
4743
|
return reply;
|
|
4597
4744
|
}
|
|
@@ -4618,6 +4765,7 @@ class ChannelOutbox {
|
|
|
4618
4765
|
* the next append (filtered to this channel + `to`) or `timeoutMs`, then return.
|
|
4619
4766
|
*/
|
|
4620
4767
|
wait(channelId, cursor, to, timeoutMs, correlationId) {
|
|
4768
|
+
this.reload();
|
|
4621
4769
|
const ready = this.since(channelId, cursor, to, correlationId);
|
|
4622
4770
|
if (ready.length) return Promise.resolve({ replies: ready, cursor: this.cursor(channelId) });
|
|
4623
4771
|
return new Promise((resolve) => {
|
|
@@ -4626,12 +4774,23 @@ class ChannelOutbox {
|
|
|
4626
4774
|
cleanup();
|
|
4627
4775
|
resolve({ replies: this.since(channelId, cursor, to, correlationId), cursor: this.cursor(channelId) });
|
|
4628
4776
|
};
|
|
4777
|
+
const diskPoll = setInterval(() => {
|
|
4778
|
+
if (this.reload()) {
|
|
4779
|
+
const more = this.since(channelId, cursor, to, correlationId);
|
|
4780
|
+
if (more.length) {
|
|
4781
|
+
cleanup();
|
|
4782
|
+
resolve({ replies: more, cursor: this.cursor(channelId) });
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
}, 1e3);
|
|
4786
|
+
diskPoll.unref?.();
|
|
4629
4787
|
const timer = setTimeout(() => {
|
|
4630
4788
|
cleanup();
|
|
4631
4789
|
resolve({ replies: [], cursor: this.cursor(channelId) });
|
|
4632
4790
|
}, Math.max(0, timeoutMs));
|
|
4633
4791
|
const cleanup = () => {
|
|
4634
4792
|
clearTimeout(timer);
|
|
4793
|
+
clearInterval(diskPoll);
|
|
4635
4794
|
this.emitter.off(channelId, onReply);
|
|
4636
4795
|
};
|
|
4637
4796
|
this.emitter.on(channelId, onReply);
|
|
@@ -4650,11 +4809,57 @@ class ChannelOutbox {
|
|
|
4650
4809
|
if (existsSync(this.file)) this.reload();
|
|
4651
4810
|
this.byChannel.delete(channelId);
|
|
4652
4811
|
this.seqByChannel.delete(channelId);
|
|
4812
|
+
if (this.highWater.delete(channelId)) this._persistHighWater();
|
|
4653
4813
|
if (!existsSync(this.file)) return;
|
|
4654
4814
|
this._compact();
|
|
4655
4815
|
}
|
|
4656
4816
|
}
|
|
4657
4817
|
|
|
4818
|
+
const SESSION_METHOD_MIN_ROLE = {
|
|
4819
|
+
// interact
|
|
4820
|
+
sendMessage: "interact",
|
|
4821
|
+
abort: "interact",
|
|
4822
|
+
permissionResponse: "interact",
|
|
4823
|
+
regenerateSummary: "interact",
|
|
4824
|
+
keepAlive: "interact",
|
|
4825
|
+
sessionEnd: "interact",
|
|
4826
|
+
markInboxRead: "interact",
|
|
4827
|
+
sendInboxMessage: "interact",
|
|
4828
|
+
// admin
|
|
4829
|
+
updateMetadata: "admin",
|
|
4830
|
+
updateConfig: "admin",
|
|
4831
|
+
saveChannel: "admin",
|
|
4832
|
+
removeChannel: "admin",
|
|
4833
|
+
setChannelEnabled: "admin",
|
|
4834
|
+
addChannelCaller: "admin",
|
|
4835
|
+
outpostConnectInfo: "admin",
|
|
4836
|
+
outpostExec: "admin",
|
|
4837
|
+
channelReply: "admin",
|
|
4838
|
+
updateAgentState: "admin",
|
|
4839
|
+
switchMode: "admin",
|
|
4840
|
+
restartClaude: "admin",
|
|
4841
|
+
archiveSession: "admin",
|
|
4842
|
+
readFile: "admin",
|
|
4843
|
+
writeFile: "admin",
|
|
4844
|
+
listDirectory: "admin",
|
|
4845
|
+
bash: "admin",
|
|
4846
|
+
issue: "admin",
|
|
4847
|
+
workflow: "admin",
|
|
4848
|
+
ripgrep: "admin",
|
|
4849
|
+
getDirectoryTree: "admin",
|
|
4850
|
+
updateSharing: "admin",
|
|
4851
|
+
updateSecurityContext: "admin",
|
|
4852
|
+
setClaudeChrome: "admin",
|
|
4853
|
+
applySystemPrompt: "admin",
|
|
4854
|
+
clearInbox: "admin",
|
|
4855
|
+
btw: "admin",
|
|
4856
|
+
editMessage: "admin",
|
|
4857
|
+
refineLastReply: "admin",
|
|
4858
|
+
undoLastEdit: "admin"
|
|
4859
|
+
};
|
|
4860
|
+
function sessionMethodMinRole(method) {
|
|
4861
|
+
return SESSION_METHOD_MIN_ROLE[method] ?? "view";
|
|
4862
|
+
}
|
|
4658
4863
|
function getParamNames(fn) {
|
|
4659
4864
|
const src = fn.toString();
|
|
4660
4865
|
const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
@@ -4909,9 +5114,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
4909
5114
|
} catch {
|
|
4910
5115
|
}
|
|
4911
5116
|
if (hasMachineAccess) {
|
|
5117
|
+
const { securityContextConfig: _sc2, ...safeMetadata2 } = currentMetadata;
|
|
4912
5118
|
return {
|
|
4913
5119
|
machineId,
|
|
4914
|
-
metadata:
|
|
5120
|
+
metadata: safeMetadata2,
|
|
4915
5121
|
metadataVersion,
|
|
4916
5122
|
daemonState: currentDaemonState,
|
|
4917
5123
|
daemonStateVersion
|
|
@@ -4922,11 +5128,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
4922
5128
|
for (const sid of sessionIds) {
|
|
4923
5129
|
const rpc = handlers.getSessionRPCHandlers?.(sid);
|
|
4924
5130
|
if (!rpc) continue;
|
|
4925
|
-
|
|
4926
|
-
await rpc.getMetadata(context);
|
|
5131
|
+
if (await hasExplicitSessionAccess(rpc, "view", context)) {
|
|
4927
5132
|
hasSessionAccess = true;
|
|
4928
5133
|
break;
|
|
4929
|
-
} catch {
|
|
4930
5134
|
}
|
|
4931
5135
|
}
|
|
4932
5136
|
if (!hasSessionAccess) {
|
|
@@ -5031,10 +5235,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5031
5235
|
if (!rpc) {
|
|
5032
5236
|
throw new Error(`Session ${sessionId} not found on this machine`);
|
|
5033
5237
|
}
|
|
5238
|
+
const requiredRole = sessionMethodMinRole(method);
|
|
5034
5239
|
try {
|
|
5035
|
-
authorizeRequest(context, currentMetadata.sharing,
|
|
5240
|
+
authorizeRequest(context, currentMetadata.sharing, requiredRole);
|
|
5036
5241
|
} catch (machineErr) {
|
|
5037
|
-
if (!await hasExplicitSessionAccess(rpc,
|
|
5242
|
+
if (!await hasExplicitSessionAccess(rpc, requiredRole, context)) throw machineErr;
|
|
5038
5243
|
}
|
|
5039
5244
|
const handler = rpc[method];
|
|
5040
5245
|
if (typeof handler !== "function") {
|
|
@@ -5883,7 +6088,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5883
6088
|
const tunnels = handlers.tunnels;
|
|
5884
6089
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
5885
6090
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
5886
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
6091
|
+
const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
|
|
5887
6092
|
const tunnel = new FrpcTunnel({
|
|
5888
6093
|
name: params.name,
|
|
5889
6094
|
ports: params.ports,
|
|
@@ -5998,6 +6203,14 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5998
6203
|
const { homedir } = await import('os');
|
|
5999
6204
|
assertNonAdminMountDirSafe(params.directory, homedir());
|
|
6000
6205
|
}
|
|
6206
|
+
const existingMount = sm.getMount(params.name);
|
|
6207
|
+
if (existingMount && !serveCallerTrusted(context)) {
|
|
6208
|
+
const callerEmail = (context?.user?.email || "").toLowerCase();
|
|
6209
|
+
const mountOwner = (existingMount.ownerEmail || "").toLowerCase();
|
|
6210
|
+
if (!callerEmail || !mountOwner || callerEmail !== mountOwner) {
|
|
6211
|
+
throw new Error(`Not authorized to replace mount '${params.name}' (owned by another user)`);
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6001
6214
|
const ownerEmail2 = params.ownerEmail || context?.user?.email || currentMetadata.sharing?.owner || process.env.SVAMP_OWNER_EMAIL || void 0;
|
|
6002
6215
|
const access = params.access ?? "owner";
|
|
6003
6216
|
return sm.applyMount({
|
|
@@ -6014,6 +6227,14 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6014
6227
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
6015
6228
|
const sm = handlers.serveManager;
|
|
6016
6229
|
if (!sm) throw new Error("Serve manager not available");
|
|
6230
|
+
const target = sm.getMount(params.name);
|
|
6231
|
+
if (target && !serveCallerTrusted(context)) {
|
|
6232
|
+
const callerEmail = (context?.user?.email || "").toLowerCase();
|
|
6233
|
+
const mountOwner = (target.ownerEmail || "").toLowerCase();
|
|
6234
|
+
if (!callerEmail || !mountOwner || callerEmail !== mountOwner) {
|
|
6235
|
+
throw new Error(`Not authorized to remove mount '${params.name}' (owned by another user)`);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6017
6238
|
await sm.removeMount(params.name);
|
|
6018
6239
|
return { removed: true };
|
|
6019
6240
|
},
|
|
@@ -6033,7 +6254,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6033
6254
|
if (!sm) throw new Error("Serve manager not available");
|
|
6034
6255
|
const info = sm.getInfo();
|
|
6035
6256
|
const isAdmin = serveCallerTrusted(context);
|
|
6036
|
-
|
|
6257
|
+
const mounts = (info.mounts || []).map((m) => sanitizeMountForRole(m, isAdmin));
|
|
6258
|
+
const topUrl = isAdmin ? info.url : mounts[0]?.url ?? null;
|
|
6259
|
+
return { ...info, url: topUrl, mounts };
|
|
6037
6260
|
},
|
|
6038
6261
|
/**
|
|
6039
6262
|
* Aggregate frpc tunnel health for all serve mounts.
|
|
@@ -6359,7 +6582,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6359
6582
|
}
|
|
6360
6583
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
6361
6584
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
6362
|
-
const { toolsForRole } = await import('./sideband-
|
|
6585
|
+
const { toolsForRole } = await import('./sideband-CdJ0O6L6.mjs');
|
|
6363
6586
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
6364
6587
|
return fmt(r2);
|
|
6365
6588
|
}
|
|
@@ -6464,7 +6687,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6464
6687
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
6465
6688
|
}
|
|
6466
6689
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
6467
|
-
const { queryCore } = await import('./commands
|
|
6690
|
+
const { queryCore } = await import('./commands-CJVqMnNf.mjs');
|
|
6468
6691
|
const timeout = c.reply?.timeout_sec || 120;
|
|
6469
6692
|
let result;
|
|
6470
6693
|
try {
|
|
@@ -6742,6 +6965,48 @@ const AWAIT_MAX = num("SVAMP_INBOX_AWAIT_MAX", 200);
|
|
|
6742
6965
|
function awaitPath(sessionId) {
|
|
6743
6966
|
return join$1(SVAMP_HOME$2, "awaiting", `${sessionId}.json`);
|
|
6744
6967
|
}
|
|
6968
|
+
const _sleepSab = new Int32Array(new SharedArrayBuffer(4));
|
|
6969
|
+
function sleepSync(ms) {
|
|
6970
|
+
try {
|
|
6971
|
+
Atomics.wait(_sleepSab, 0, 0, ms);
|
|
6972
|
+
} catch {
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6975
|
+
function withAwaitLock(sessionId, fn) {
|
|
6976
|
+
const lock = awaitPath(sessionId) + ".lock";
|
|
6977
|
+
try {
|
|
6978
|
+
mkdirSync$1(join$1(SVAMP_HOME$2, "awaiting"), { recursive: true });
|
|
6979
|
+
} catch {
|
|
6980
|
+
}
|
|
6981
|
+
const deadline = Date.now() + 50;
|
|
6982
|
+
let held = false;
|
|
6983
|
+
while (Date.now() < deadline) {
|
|
6984
|
+
try {
|
|
6985
|
+
mkdirSync$1(lock);
|
|
6986
|
+
held = true;
|
|
6987
|
+
break;
|
|
6988
|
+
} catch {
|
|
6989
|
+
try {
|
|
6990
|
+
if (Date.now() - statSync(lock).mtimeMs > 5e3) {
|
|
6991
|
+
rmSync$1(lock, { recursive: true, force: true });
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
6994
|
+
} catch {
|
|
6995
|
+
}
|
|
6996
|
+
sleepSync(5);
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
try {
|
|
7000
|
+
return fn();
|
|
7001
|
+
} finally {
|
|
7002
|
+
if (held) {
|
|
7003
|
+
try {
|
|
7004
|
+
rmSync$1(lock, { recursive: true, force: true });
|
|
7005
|
+
} catch {
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
6745
7010
|
function readAwaiting(sessionId) {
|
|
6746
7011
|
try {
|
|
6747
7012
|
const p = awaitPath(sessionId);
|
|
@@ -6764,14 +7029,16 @@ function writeAwaiting(sessionId, map) {
|
|
|
6764
7029
|
}
|
|
6765
7030
|
function registerAwaitingReply(sessionId, threadId, now = Date.now()) {
|
|
6766
7031
|
if (!sessionId || !threadId) return;
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
7032
|
+
withAwaitLock(sessionId, () => {
|
|
7033
|
+
const map = readAwaiting(sessionId);
|
|
7034
|
+
for (const k of Object.keys(map)) if (now - map[k] > AWAIT_TTL_MS) delete map[k];
|
|
7035
|
+
map[threadId] = now;
|
|
7036
|
+
const keys = Object.keys(map);
|
|
7037
|
+
if (keys.length > AWAIT_MAX) {
|
|
7038
|
+
keys.sort((a, b) => map[a] - map[b]).slice(0, keys.length - AWAIT_MAX).forEach((k) => delete map[k]);
|
|
7039
|
+
}
|
|
7040
|
+
writeAwaiting(sessionId, map);
|
|
7041
|
+
});
|
|
6775
7042
|
}
|
|
6776
7043
|
function peekAwaitingReply(sessionId, threadId, now = Date.now()) {
|
|
6777
7044
|
if (!sessionId || !threadId) return false;
|
|
@@ -6782,10 +7049,12 @@ function peekAwaitingReply(sessionId, threadId, now = Date.now()) {
|
|
|
6782
7049
|
}
|
|
6783
7050
|
function consumeAwaitingReply(sessionId, threadId) {
|
|
6784
7051
|
if (!sessionId || !threadId) return;
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
7052
|
+
withAwaitLock(sessionId, () => {
|
|
7053
|
+
const map = readAwaiting(sessionId);
|
|
7054
|
+
if (!(threadId in map)) return;
|
|
7055
|
+
delete map[threadId];
|
|
7056
|
+
writeAwaiting(sessionId, map);
|
|
7057
|
+
});
|
|
6789
7058
|
}
|
|
6790
7059
|
function computeOutboundHop(sessionId) {
|
|
6791
7060
|
if (!sessionId) return { hopCount: 0, fromInboxTurn: false };
|
|
@@ -6908,7 +7177,7 @@ function saveInbox(projectDir, sessionId, inbox) {
|
|
|
6908
7177
|
try {
|
|
6909
7178
|
const p = inboxFilePath(projectDir, sessionId);
|
|
6910
7179
|
mkdirSync$1(dirname(p), { recursive: true });
|
|
6911
|
-
const tmp = `${p}.tmp-${process.pid}`;
|
|
7180
|
+
const tmp = `${p}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
6912
7181
|
writeFileSync$1(tmp, JSON.stringify(inbox));
|
|
6913
7182
|
renameSync(tmp, p);
|
|
6914
7183
|
} catch {
|
|
@@ -7188,6 +7457,7 @@ class OutpostCoordinator {
|
|
|
7188
7457
|
clearTimer;
|
|
7189
7458
|
execTimeoutMs;
|
|
7190
7459
|
staleMs;
|
|
7460
|
+
sweeper = null;
|
|
7191
7461
|
constructor(opts = {}) {
|
|
7192
7462
|
this.now = opts.now || (() => Date.now());
|
|
7193
7463
|
this.setTimer = opts.setTimer || ((fn, ms) => setTimeout(fn, ms));
|
|
@@ -7208,6 +7478,35 @@ class OutpostCoordinator {
|
|
|
7208
7478
|
}
|
|
7209
7479
|
}
|
|
7210
7480
|
}
|
|
7481
|
+
/** Public sweep — prune stale connections and fail their in-flight execs. Safe to call anytime. */
|
|
7482
|
+
sweepStale() {
|
|
7483
|
+
this.prune();
|
|
7484
|
+
}
|
|
7485
|
+
/**
|
|
7486
|
+
* Start a periodic background sweep so an in-flight exec to a machine that STOPS polling is
|
|
7487
|
+
* failed FAST (#0612) — the header's "rejected immediately with a clear disconnected error"
|
|
7488
|
+
* promise. Without this driver, prune() only ran on-demand (register/list/isConnected), so an
|
|
7489
|
+
* already-enqueued exec waited the full OUTPOST_EXEC_TIMEOUT_MS backstop. Opt-in (the daemon
|
|
7490
|
+
* enables it; the pure unit tests drive time via injected timers and never call this). Unref'd
|
|
7491
|
+
* so it never keeps the process alive; idempotent.
|
|
7492
|
+
*/
|
|
7493
|
+
startSweeper(intervalMs = Math.max(5e3, Math.floor(this.staleMs / 3))) {
|
|
7494
|
+
if (this.sweeper) return;
|
|
7495
|
+
this.sweeper = setInterval(() => {
|
|
7496
|
+
try {
|
|
7497
|
+
this.prune();
|
|
7498
|
+
} catch {
|
|
7499
|
+
}
|
|
7500
|
+
}, intervalMs);
|
|
7501
|
+
this.sweeper.unref?.();
|
|
7502
|
+
}
|
|
7503
|
+
/** Stop the background sweep (e.g. on session teardown). Idempotent. */
|
|
7504
|
+
dispose() {
|
|
7505
|
+
if (this.sweeper) {
|
|
7506
|
+
clearInterval(this.sweeper);
|
|
7507
|
+
this.sweeper = null;
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7211
7510
|
/** Reject every pending exec on a machine with a synthetic disconnect result (exitCode -1). */
|
|
7212
7511
|
failMachine(machine, reason) {
|
|
7213
7512
|
for (const [id, p] of [...this.pending]) {
|
|
@@ -7309,6 +7608,7 @@ class OutpostCoordinator {
|
|
|
7309
7608
|
resolveResult(result) {
|
|
7310
7609
|
const p = this.pending.get(result.id);
|
|
7311
7610
|
if (!p) return false;
|
|
7611
|
+
if (result.machine && result.machine !== p.machine) return false;
|
|
7312
7612
|
this.clearTimer(p.timer);
|
|
7313
7613
|
this.pending.delete(result.id);
|
|
7314
7614
|
this.touch(p.machine);
|
|
@@ -7734,6 +8034,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
7734
8034
|
const meetingActivity = [];
|
|
7735
8035
|
const channelOutbox = new ChannelOutbox(initialMetadata.path);
|
|
7736
8036
|
const outpostCoordinator = new OutpostCoordinator();
|
|
8037
|
+
outpostCoordinator.startSweeper();
|
|
7737
8038
|
const outpostProjectDir = () => metadata.path || process.cwd();
|
|
7738
8039
|
const announceOutpost = (conn) => {
|
|
7739
8040
|
const who = conn.label || conn.machine;
|
|
@@ -7804,7 +8105,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
7804
8105
|
const ensureParticipantsChannel = (sharing) => {
|
|
7805
8106
|
try {
|
|
7806
8107
|
if (sharing?.enabled) channelStore.save(buildParticipantsChannel(sharing));
|
|
7807
|
-
else
|
|
8108
|
+
else {
|
|
8109
|
+
channelStore.remove(PARTICIPANTS_CHANNEL_ID);
|
|
8110
|
+
channelOutbox.purge(PARTICIPANTS_CHANNEL_ID);
|
|
8111
|
+
}
|
|
7808
8112
|
} catch {
|
|
7809
8113
|
}
|
|
7810
8114
|
};
|
|
@@ -8228,7 +8532,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8228
8532
|
const rec = loadOutpostToken(outpostProjectDir(), sessionId);
|
|
8229
8533
|
if (!outpostTokenMatches(rec, params?.token)) return { error: "unauthorized" };
|
|
8230
8534
|
if (!params?.id) return { error: "missing result id" };
|
|
8231
|
-
const ok = outpostCoordinator.resolveResult({ id: params.id, stdout: params.stdout, stderr: params.stderr, exitCode: params.exitCode });
|
|
8535
|
+
const ok = outpostCoordinator.resolveResult({ id: params.id, stdout: params.stdout, stderr: params.stderr, exitCode: params.exitCode, machine: params.machine });
|
|
8232
8536
|
return { ok };
|
|
8233
8537
|
},
|
|
8234
8538
|
outpostExec: async (params, context) => {
|
|
@@ -8743,7 +9047,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8743
9047
|
// ── Inbox ──
|
|
8744
9048
|
sendInboxMessage: async (message, context) => {
|
|
8745
9049
|
authorizeRequest(context, metadata.sharing, "interact");
|
|
8746
|
-
const trusted = getEffectiveRole(context, metadata.sharing) === "admin";
|
|
9050
|
+
const trusted = !context?.user || isSameOwnerWorkspace(context) || !!metadata.sharing && getEffectiveRole(context, metadata.sharing) === "admin";
|
|
8747
9051
|
const msg = sanitizeInboundInboxMessage(message, { trusted, callerEmail: context?.user?.email });
|
|
8748
9052
|
inbox.push(msg);
|
|
8749
9053
|
while (inbox.length > INBOX_MAX) inbox.shift();
|
|
@@ -10175,18 +10479,56 @@ function eventAlias(eventId, recipientEmail) {
|
|
|
10175
10479
|
class SharingNotificationSync {
|
|
10176
10480
|
server;
|
|
10177
10481
|
artifactManager = null;
|
|
10482
|
+
amResolvedAt = 0;
|
|
10178
10483
|
shareCollectionId = null;
|
|
10179
10484
|
eventCollectionId = null;
|
|
10180
10485
|
initialized = false;
|
|
10181
10486
|
log;
|
|
10487
|
+
// A resolved `public/artifact-manager` proxy is bound to a SPECIFIC backend client_id. When that
|
|
10488
|
+
// backend pod is replaced (deploy/restart/idle-evict), the new instance registers under the same
|
|
10489
|
+
// public alias but a fresh client_id — the cached proxy dangles and every call TIMES OUT (~30s).
|
|
10490
|
+
// Before this fix that silently broke ALL zero-click share writes fleet-wide (empty
|
|
10491
|
+
// svamp-shared-sessions → recipients never auto-discovered). So we re-resolve on a short TTL and,
|
|
10492
|
+
// on any RPC error, force one fresh re-resolve + retry (a dead pod's proxy always fails first).
|
|
10493
|
+
static AM_TTL_MS = 3e4;
|
|
10182
10494
|
constructor(server, log) {
|
|
10183
10495
|
this.server = server;
|
|
10184
10496
|
this.log = log;
|
|
10185
10497
|
}
|
|
10498
|
+
/**
|
|
10499
|
+
* Resolve `public/artifact-manager` on the daemon's live connection. hypha-rpc re-queries the
|
|
10500
|
+
* server registry on each getService() call, so a fresh resolve yields the CURRENTLY-registered
|
|
10501
|
+
* live instance (not the dangling cached one). Cached within a short TTL to avoid a lookup per
|
|
10502
|
+
* write; `force` bypasses the cache after a failure.
|
|
10503
|
+
*/
|
|
10504
|
+
async getAM(force = false) {
|
|
10505
|
+
const now = Date.now();
|
|
10506
|
+
if (!force && this.artifactManager && now - this.amResolvedAt < SharingNotificationSync.AM_TTL_MS) {
|
|
10507
|
+
return this.artifactManager;
|
|
10508
|
+
}
|
|
10509
|
+
const am = await this.server.getService("public/artifact-manager");
|
|
10510
|
+
this.artifactManager = am;
|
|
10511
|
+
this.amResolvedAt = Date.now();
|
|
10512
|
+
return am;
|
|
10513
|
+
}
|
|
10514
|
+
/**
|
|
10515
|
+
* Run an artifact-manager operation with automatic recovery from a stale/dead proxy: try with the
|
|
10516
|
+
* (possibly cached) instance; on ANY error, force a fresh re-resolve and retry ONCE. This is the
|
|
10517
|
+
* heart of the stale-proxy fix — a replaced backend pod makes the first call time out, the retry
|
|
10518
|
+
* re-binds to the live instance and succeeds. The caller still catches the final error.
|
|
10519
|
+
*/
|
|
10520
|
+
async withAM(op) {
|
|
10521
|
+
try {
|
|
10522
|
+
return await op(await this.getAM(false));
|
|
10523
|
+
} catch (err) {
|
|
10524
|
+
this.log(`[SHARING NOTIFY] artifact-manager call failed (${err?.message}); re-resolving + retrying`);
|
|
10525
|
+
return await op(await this.getAM(true));
|
|
10526
|
+
}
|
|
10527
|
+
}
|
|
10186
10528
|
async init() {
|
|
10187
10529
|
try {
|
|
10188
|
-
|
|
10189
|
-
if (!
|
|
10530
|
+
const am = await this.getAM(true);
|
|
10531
|
+
if (!am) {
|
|
10190
10532
|
this.log("[SHARING NOTIFY] Artifact manager not available");
|
|
10191
10533
|
return;
|
|
10192
10534
|
}
|
|
@@ -10215,15 +10557,17 @@ class SharingNotificationSync {
|
|
|
10215
10557
|
shareType: params.shareType || "session"
|
|
10216
10558
|
};
|
|
10217
10559
|
try {
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10560
|
+
await this.withAM(async (am) => {
|
|
10561
|
+
const artifact = await am.create({
|
|
10562
|
+
alias,
|
|
10563
|
+
parent_id: this.shareCollectionId,
|
|
10564
|
+
type: "share-notification",
|
|
10565
|
+
stage: true,
|
|
10566
|
+
manifest,
|
|
10567
|
+
_rkwargs: true
|
|
10568
|
+
});
|
|
10569
|
+
await am.commit({ artifact_id: artifact.id, _rkwargs: true }).catch(() => {
|
|
10570
|
+
});
|
|
10227
10571
|
});
|
|
10228
10572
|
} catch (createErr) {
|
|
10229
10573
|
this.log(`[SHARING NOTIFY] share-notification attach skipped for ${params.recipientEmail}: ${createErr.message}`);
|
|
@@ -10265,14 +10609,16 @@ class SharingNotificationSync {
|
|
|
10265
10609
|
if (!this.initialized || !this.shareCollectionId) return;
|
|
10266
10610
|
const alias = shareAlias(sessionId, recipientEmail);
|
|
10267
10611
|
try {
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10612
|
+
await this.withAM(async (am) => {
|
|
10613
|
+
const existing = await am.read({
|
|
10614
|
+
artifact_id: alias,
|
|
10615
|
+
parent_id: this.shareCollectionId,
|
|
10616
|
+
_rkwargs: true
|
|
10617
|
+
});
|
|
10618
|
+
await am.delete({
|
|
10619
|
+
artifact_id: existing.id,
|
|
10620
|
+
_rkwargs: true
|
|
10621
|
+
});
|
|
10276
10622
|
});
|
|
10277
10623
|
this.log(`[SHARING NOTIFY] Removed share notification for ${recipientEmail} on ${sessionId.slice(0, 8)}`);
|
|
10278
10624
|
} catch {
|
|
@@ -10303,17 +10649,19 @@ class SharingNotificationSync {
|
|
|
10303
10649
|
if (!this.initialized || !this.eventCollectionId) return;
|
|
10304
10650
|
const alias = eventAlias(event.id, event.recipientEmail);
|
|
10305
10651
|
try {
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10652
|
+
await this.withAM(async (am) => {
|
|
10653
|
+
const artifact = await am.create({
|
|
10654
|
+
alias,
|
|
10655
|
+
parent_id: this.eventCollectionId,
|
|
10656
|
+
type: "user-event",
|
|
10657
|
+
stage: true,
|
|
10658
|
+
manifest: event,
|
|
10659
|
+
_rkwargs: true
|
|
10660
|
+
});
|
|
10661
|
+
await am.commit({
|
|
10662
|
+
artifact_id: artifact.id,
|
|
10663
|
+
_rkwargs: true
|
|
10664
|
+
});
|
|
10317
10665
|
});
|
|
10318
10666
|
this.log(`[USER EVENT] Published ${event.type} to ${event.recipientEmail}`);
|
|
10319
10667
|
try {
|
|
@@ -10340,7 +10688,7 @@ class SharingNotificationSync {
|
|
|
10340
10688
|
const all = [];
|
|
10341
10689
|
const seen = /* @__PURE__ */ new Set();
|
|
10342
10690
|
for (let offset = 0; ; offset += PAGE) {
|
|
10343
|
-
const page = await this.
|
|
10691
|
+
const page = await this.withAM((am) => am.list({ parent_id: parentId, offset, limit: PAGE, _rkwargs: true }));
|
|
10344
10692
|
if (!Array.isArray(page) || page.length === 0) break;
|
|
10345
10693
|
let fresh = 0;
|
|
10346
10694
|
for (const c of page) {
|
|
@@ -10381,7 +10729,7 @@ class SharingNotificationSync {
|
|
|
10381
10729
|
live.push(m);
|
|
10382
10730
|
}
|
|
10383
10731
|
for (const id of expiredOwnedIds) {
|
|
10384
|
-
this.
|
|
10732
|
+
this.withAM((am) => am.delete({ artifact_id: id, _rkwargs: true })).catch(() => {
|
|
10385
10733
|
});
|
|
10386
10734
|
}
|
|
10387
10735
|
return live.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
@@ -10407,7 +10755,7 @@ class SharingNotificationSync {
|
|
|
10407
10755
|
return m && typeof m.id === "string" && (m.id === eventId || m.id.slice(0, 8) === short) && typeof m.recipientEmail === "string" && m.recipientEmail.toLowerCase() === normalized;
|
|
10408
10756
|
});
|
|
10409
10757
|
if (!match) return false;
|
|
10410
|
-
await this.
|
|
10758
|
+
await this.withAM((am) => am.delete({ artifact_id: match.id, _rkwargs: true }));
|
|
10411
10759
|
this.log(`[USER EVENT] Acked ${eventId.slice(0, 8)} for ${recipientEmail}`);
|
|
10412
10760
|
return true;
|
|
10413
10761
|
} catch (err) {
|
|
@@ -13517,7 +13865,13 @@ const MAX_RESTART_DELAY_S = 300;
|
|
|
13517
13865
|
const BACKOFF_RESET_WINDOW_MS = 6e4;
|
|
13518
13866
|
const MAX_LOG_LINES = 300;
|
|
13519
13867
|
const CRASH_LOOP_FAILURE_THRESHOLD = 10;
|
|
13520
|
-
|
|
13868
|
+
function accumulateCrashLoopFailure(prev, healthy, now) {
|
|
13869
|
+
if (healthy) return { consecutiveFailures: 0, failureWindowStart: void 0 };
|
|
13870
|
+
return {
|
|
13871
|
+
consecutiveFailures: prev.consecutiveFailures + 1,
|
|
13872
|
+
failureWindowStart: prev.failureWindowStart ?? now
|
|
13873
|
+
};
|
|
13874
|
+
}
|
|
13521
13875
|
class ProcessSupervisor {
|
|
13522
13876
|
entries = /* @__PURE__ */ new Map();
|
|
13523
13877
|
persistDir;
|
|
@@ -13924,17 +14278,13 @@ class ProcessSupervisor {
|
|
|
13924
14278
|
if (!spec.keepAlive) return;
|
|
13925
14279
|
const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
|
|
13926
14280
|
const now = Date.now();
|
|
13927
|
-
|
|
13928
|
-
state.consecutiveFailures
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
} else {
|
|
13935
|
-
state.consecutiveFailures++;
|
|
13936
|
-
}
|
|
13937
|
-
}
|
|
14281
|
+
const acct = accumulateCrashLoopFailure(
|
|
14282
|
+
{ consecutiveFailures: state.consecutiveFailures, failureWindowStart: entry.failureWindowStart },
|
|
14283
|
+
uptime > BACKOFF_RESET_WINDOW_MS,
|
|
14284
|
+
now
|
|
14285
|
+
);
|
|
14286
|
+
state.consecutiveFailures = acct.consecutiveFailures;
|
|
14287
|
+
entry.failureWindowStart = acct.failureWindowStart;
|
|
13938
14288
|
if (state.consecutiveFailures >= CRASH_LOOP_FAILURE_THRESHOLD) {
|
|
13939
14289
|
const windowS = Math.round((now - (entry.failureWindowStart ?? now)) / 1e3);
|
|
13940
14290
|
state.status = "crash-loop-backoff";
|
|
@@ -14193,22 +14543,29 @@ function parseIssue(content) {
|
|
|
14193
14543
|
body: (m[2] || "").trim() || void 0
|
|
14194
14544
|
};
|
|
14195
14545
|
}
|
|
14196
|
-
function
|
|
14546
|
+
function readDirWithMtime(dir) {
|
|
14197
14547
|
if (!existsSync(dir)) return [];
|
|
14198
14548
|
const out = [];
|
|
14199
14549
|
for (const name of readdirSync(dir)) {
|
|
14200
14550
|
if (!name.endsWith(".md")) continue;
|
|
14201
14551
|
try {
|
|
14202
|
-
const
|
|
14203
|
-
|
|
14552
|
+
const p = join$1(dir, name);
|
|
14553
|
+
const issue = parseIssue(readFileSync(p, "utf-8"));
|
|
14554
|
+
if (issue) out.push({ issue, mtimeMs: statSync(p).mtimeMs });
|
|
14204
14555
|
} catch {
|
|
14205
14556
|
}
|
|
14206
14557
|
}
|
|
14207
14558
|
return out;
|
|
14208
14559
|
}
|
|
14209
14560
|
function listIssues(projectRoot, opts = {}) {
|
|
14210
|
-
|
|
14211
|
-
if (opts.includeArchived)
|
|
14561
|
+
const entries = readDirWithMtime(issuesDir(projectRoot));
|
|
14562
|
+
if (opts.includeArchived) entries.push(...readDirWithMtime(archiveDir(projectRoot)));
|
|
14563
|
+
const byId = /* @__PURE__ */ new Map();
|
|
14564
|
+
for (const e of entries) {
|
|
14565
|
+
const prev = byId.get(e.issue.id);
|
|
14566
|
+
if (!prev || e.mtimeMs >= prev.mtimeMs) byId.set(e.issue.id, e);
|
|
14567
|
+
}
|
|
14568
|
+
let items = Array.from(byId.values()).map((e) => e.issue);
|
|
14212
14569
|
if (opts.status) items = items.filter((i) => i.status === opts.status);
|
|
14213
14570
|
if (opts.label) items = items.filter((i) => i.labels.includes(opts.label));
|
|
14214
14571
|
if (opts.scope) items = items.filter((i) => i.scope === opts.scope);
|
|
@@ -14223,20 +14580,22 @@ function isVisibleTo(issue, session) {
|
|
|
14223
14580
|
}
|
|
14224
14581
|
function getIssue(projectRoot, id) {
|
|
14225
14582
|
const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
|
|
14583
|
+
let best = null;
|
|
14226
14584
|
for (const p of [issuePath(projectRoot, padded), issuePath(projectRoot, padded, true)]) {
|
|
14227
|
-
if (existsSync(p))
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
}
|
|
14585
|
+
if (!existsSync(p)) continue;
|
|
14586
|
+
try {
|
|
14587
|
+
const issue = parseIssue(readFileSync(p, "utf-8"));
|
|
14588
|
+
if (!issue) continue;
|
|
14589
|
+
const mtimeMs = statSync(p).mtimeMs;
|
|
14590
|
+
if (!best || mtimeMs >= best.mtimeMs) best = { issue, mtimeMs };
|
|
14591
|
+
} catch {
|
|
14233
14592
|
}
|
|
14234
14593
|
}
|
|
14235
|
-
return null;
|
|
14594
|
+
return best ? best.issue : null;
|
|
14236
14595
|
}
|
|
14237
14596
|
function atomicWrite(path, content) {
|
|
14238
14597
|
mkdirSync$1(dirname(path), { recursive: true });
|
|
14239
|
-
const tmp = `${path}.tmp-${process.pid}`;
|
|
14598
|
+
const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
14240
14599
|
writeFileSync$1(tmp, content);
|
|
14241
14600
|
renameSync(tmp, path);
|
|
14242
14601
|
}
|
|
@@ -14280,7 +14639,18 @@ function addIssue(projectRoot, fields) {
|
|
|
14280
14639
|
}
|
|
14281
14640
|
throw new Error("addIssue: could not allocate a free issue id");
|
|
14282
14641
|
}
|
|
14283
|
-
function
|
|
14642
|
+
function issueLockPath(projectRoot, id) {
|
|
14643
|
+
const padded = /^\d+$/.test(id) ? id.padStart(4, "0") : id;
|
|
14644
|
+
return join$1(issuesDir(projectRoot), `${padded}.lock`);
|
|
14645
|
+
}
|
|
14646
|
+
function withIssueLock(projectRoot, id, fn) {
|
|
14647
|
+
try {
|
|
14648
|
+
mkdirSync$1(issuesDir(projectRoot), { recursive: true });
|
|
14649
|
+
} catch {
|
|
14650
|
+
}
|
|
14651
|
+
return withFileLock(issueLockPath(projectRoot, id), fn);
|
|
14652
|
+
}
|
|
14653
|
+
function _updateIssueUnlocked(projectRoot, id, patch) {
|
|
14284
14654
|
const cur = getIssue(projectRoot, id);
|
|
14285
14655
|
if (!cur) return null;
|
|
14286
14656
|
const wasArchived = cur.status === "archived";
|
|
@@ -14299,17 +14669,23 @@ function updateIssue(projectRoot, id, patch) {
|
|
|
14299
14669
|
}
|
|
14300
14670
|
return next;
|
|
14301
14671
|
}
|
|
14672
|
+
function updateIssue(projectRoot, id, patch) {
|
|
14673
|
+
return withIssueLock(projectRoot, id, () => _updateIssueUnlocked(projectRoot, id, patch));
|
|
14674
|
+
}
|
|
14302
14675
|
function addComment(projectRoot, id, text) {
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14676
|
+
if (!text.trim()) return getIssue(projectRoot, id);
|
|
14677
|
+
return withIssueLock(projectRoot, id, () => {
|
|
14678
|
+
const cur = getIssue(projectRoot, id);
|
|
14679
|
+
if (!cur) return null;
|
|
14680
|
+
const entry = `
|
|
14306
14681
|
|
|
14307
14682
|
---
|
|
14308
14683
|
**Follow-up \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}**
|
|
14309
14684
|
|
|
14310
14685
|
${text.trim()}`;
|
|
14311
|
-
|
|
14312
|
-
|
|
14686
|
+
const body = (cur.body ? cur.body.replace(/\s+$/, "") : "") + entry;
|
|
14687
|
+
return _updateIssueUnlocked(projectRoot, id, { body });
|
|
14688
|
+
});
|
|
14313
14689
|
}
|
|
14314
14690
|
function pauseIssue(projectRoot, id, question) {
|
|
14315
14691
|
const cur = getIssue(projectRoot, id);
|
|
@@ -14376,8 +14752,14 @@ function workflowCrons(wf) {
|
|
|
14376
14752
|
function workflowsDir(projectRoot) {
|
|
14377
14753
|
return join$1(projectRoot, ".svamp", "workflows");
|
|
14378
14754
|
}
|
|
14755
|
+
function validateWorkflowName(name) {
|
|
14756
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
14757
|
+
throw new Error(`Invalid workflow name "${name}" \u2014 allowed: letters, digits, dot, dash, underscore (no path separators).`);
|
|
14758
|
+
}
|
|
14759
|
+
return name;
|
|
14760
|
+
}
|
|
14379
14761
|
function workflowPath(projectRoot, name) {
|
|
14380
|
-
return join$1(workflowsDir(projectRoot), `${name}.yaml`);
|
|
14762
|
+
return join$1(workflowsDir(projectRoot), `${validateWorkflowName(name)}.yaml`);
|
|
14381
14763
|
}
|
|
14382
14764
|
function normalizeOn(on) {
|
|
14383
14765
|
if (!on || typeof on !== "object") {
|
|
@@ -14469,7 +14851,12 @@ function listWorkflows(projectRoot) {
|
|
|
14469
14851
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
14470
14852
|
}
|
|
14471
14853
|
function getWorkflow(projectRoot, name) {
|
|
14472
|
-
|
|
14854
|
+
let p;
|
|
14855
|
+
try {
|
|
14856
|
+
p = workflowPath(projectRoot, name);
|
|
14857
|
+
} catch {
|
|
14858
|
+
return null;
|
|
14859
|
+
}
|
|
14473
14860
|
if (!existsSync(p)) return null;
|
|
14474
14861
|
try {
|
|
14475
14862
|
return parseWorkflow(readFileSync(p, "utf-8"));
|
|
@@ -14478,14 +14865,19 @@ function getWorkflow(projectRoot, name) {
|
|
|
14478
14865
|
}
|
|
14479
14866
|
}
|
|
14480
14867
|
function rawWorkflow(projectRoot, name) {
|
|
14481
|
-
|
|
14868
|
+
let p;
|
|
14869
|
+
try {
|
|
14870
|
+
p = workflowPath(projectRoot, name);
|
|
14871
|
+
} catch {
|
|
14872
|
+
return null;
|
|
14873
|
+
}
|
|
14482
14874
|
return existsSync(p) ? readFileSync(p, "utf-8") : null;
|
|
14483
14875
|
}
|
|
14484
14876
|
function saveWorkflow(projectRoot, wf) {
|
|
14485
14877
|
const dir = workflowsDir(projectRoot);
|
|
14486
14878
|
mkdirSync$1(dir, { recursive: true });
|
|
14487
14879
|
const path = workflowPath(projectRoot, wf.name);
|
|
14488
|
-
const tmp = `${path}.tmp-${process.pid}`;
|
|
14880
|
+
const tmp = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
14489
14881
|
writeFileSync$1(tmp, serializeWorkflow(wf));
|
|
14490
14882
|
renameSync(tmp, path);
|
|
14491
14883
|
}
|
|
@@ -14517,6 +14909,10 @@ function maxRunsPerWorkflow() {
|
|
|
14517
14909
|
}
|
|
14518
14910
|
const MAX_STREAM_CHARS = 8e3;
|
|
14519
14911
|
const STEP_TIMEOUT_MS = 12e4;
|
|
14912
|
+
function stepTimeoutMs() {
|
|
14913
|
+
const v = Number(process.env.SVAMP_WORKFLOW_STEP_TIMEOUT_MS);
|
|
14914
|
+
return Number.isFinite(v) && v > 0 ? v : STEP_TIMEOUT_MS;
|
|
14915
|
+
}
|
|
14520
14916
|
function capStream(s, max = MAX_STREAM_CHARS) {
|
|
14521
14917
|
if (s.length <= max) return s;
|
|
14522
14918
|
const head = Math.floor(max * 0.6);
|
|
@@ -14546,14 +14942,16 @@ function recordRun(projectRoot, run) {
|
|
|
14546
14942
|
const dir = runsDir(projectRoot);
|
|
14547
14943
|
if (!existsSync(dir)) mkdirSync$1(dir, { recursive: true });
|
|
14548
14944
|
const file = runsFile(projectRoot, run.workflow);
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14945
|
+
withFileLock(`${file}.lock`, () => {
|
|
14946
|
+
const existing = loadRuns(projectRoot, run.workflow);
|
|
14947
|
+
const idx = existing.findIndex((r) => r.id === run.id);
|
|
14948
|
+
if (idx >= 0) existing[idx] = run;
|
|
14949
|
+
else existing.push(run);
|
|
14950
|
+
const kept = existing.slice(-maxRunsPerWorkflow());
|
|
14951
|
+
const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
14952
|
+
writeFileSync$1(tmp, kept.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
14953
|
+
renameSync(tmp, file);
|
|
14954
|
+
});
|
|
14557
14955
|
} catch {
|
|
14558
14956
|
}
|
|
14559
14957
|
}
|
|
@@ -14589,6 +14987,48 @@ function listRuns(projectRoot, workflow, opts = {}) {
|
|
|
14589
14987
|
function getRun(projectRoot, workflow, runId) {
|
|
14590
14988
|
return loadRuns(projectRoot, workflow).find((r) => r.id === runId) || null;
|
|
14591
14989
|
}
|
|
14990
|
+
function reconcileInterruptedRuns(projectRoot) {
|
|
14991
|
+
let fixed = 0;
|
|
14992
|
+
try {
|
|
14993
|
+
const dir = runsDir(projectRoot);
|
|
14994
|
+
if (!existsSync(dir)) return 0;
|
|
14995
|
+
for (const entry of readdirSync(dir)) {
|
|
14996
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
14997
|
+
const file = join$1(dir, entry);
|
|
14998
|
+
let runs;
|
|
14999
|
+
try {
|
|
15000
|
+
runs = readFileSync(file, "utf-8").split("\n").filter(Boolean).map((l) => {
|
|
15001
|
+
try {
|
|
15002
|
+
return JSON.parse(l);
|
|
15003
|
+
} catch {
|
|
15004
|
+
return null;
|
|
15005
|
+
}
|
|
15006
|
+
}).filter((r) => !!r);
|
|
15007
|
+
} catch {
|
|
15008
|
+
continue;
|
|
15009
|
+
}
|
|
15010
|
+
let changed = false;
|
|
15011
|
+
for (const r of runs) {
|
|
15012
|
+
if (r.status !== "running") continue;
|
|
15013
|
+
if (isWorkflowInFlight(projectRoot, r.workflow)) continue;
|
|
15014
|
+
r.status = "failed";
|
|
15015
|
+
r.error = r.error || "interrupted by daemon restart";
|
|
15016
|
+
r.finishedAt = r.finishedAt || r.startedAt;
|
|
15017
|
+
changed = true;
|
|
15018
|
+
fixed++;
|
|
15019
|
+
}
|
|
15020
|
+
if (!changed) continue;
|
|
15021
|
+
try {
|
|
15022
|
+
const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
15023
|
+
writeFileSync$1(tmp, runs.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
15024
|
+
renameSync(tmp, file);
|
|
15025
|
+
} catch {
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
} catch {
|
|
15029
|
+
}
|
|
15030
|
+
return fixed;
|
|
15031
|
+
}
|
|
14592
15032
|
function defaultExecStep(root, cmd, env) {
|
|
14593
15033
|
return new Promise((resolve) => {
|
|
14594
15034
|
let stdout = "", stderr = "", timedOut = false, settled = false;
|
|
@@ -14598,14 +15038,18 @@ function defaultExecStep(root, cmd, env) {
|
|
|
14598
15038
|
resolve({ exitCode, stdout, stderr, timedOut });
|
|
14599
15039
|
};
|
|
14600
15040
|
try {
|
|
14601
|
-
const child = spawn$1("sh", ["-c", cmd], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
15041
|
+
const child = spawn$1("sh", ["-c", cmd], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
14602
15042
|
const timer = setTimeout(() => {
|
|
14603
15043
|
timedOut = true;
|
|
14604
15044
|
try {
|
|
14605
|
-
child.kill("SIGKILL");
|
|
15045
|
+
if (child.pid) process.kill(-child.pid, "SIGKILL");
|
|
14606
15046
|
} catch {
|
|
15047
|
+
try {
|
|
15048
|
+
child.kill("SIGKILL");
|
|
15049
|
+
} catch {
|
|
15050
|
+
}
|
|
14607
15051
|
}
|
|
14608
|
-
},
|
|
15052
|
+
}, stepTimeoutMs());
|
|
14609
15053
|
child.stdout?.on("data", (d) => {
|
|
14610
15054
|
stdout += d.toString();
|
|
14611
15055
|
if (stdout.length > MAX_STREAM_CHARS * 4) stdout = capStream(stdout);
|
|
@@ -14684,6 +15128,9 @@ const inFlightRuns = /* @__PURE__ */ new Map();
|
|
|
14684
15128
|
function workflowLockKey(projectRoot, name) {
|
|
14685
15129
|
return `${projectRoot}\0${name}`;
|
|
14686
15130
|
}
|
|
15131
|
+
function isWorkflowInFlight(projectRoot, name) {
|
|
15132
|
+
return inFlightRuns.has(workflowLockKey(projectRoot, name));
|
|
15133
|
+
}
|
|
14687
15134
|
function runWorkflow(projectRoot, wf, opts) {
|
|
14688
15135
|
if (opts.allowConcurrent) return runWorkflowInner(projectRoot, wf, opts);
|
|
14689
15136
|
const key = workflowLockKey(projectRoot, wf.name);
|
|
@@ -14760,6 +15207,25 @@ async function runWorkflowInner(projectRoot, wf, opts) {
|
|
|
14760
15207
|
return run;
|
|
14761
15208
|
}
|
|
14762
15209
|
|
|
15210
|
+
var runStore = /*#__PURE__*/Object.freeze({
|
|
15211
|
+
__proto__: null,
|
|
15212
|
+
MAX_RUNS_PER_WORKFLOW: MAX_RUNS_PER_WORKFLOW,
|
|
15213
|
+
MAX_STREAM_CHARS: MAX_STREAM_CHARS,
|
|
15214
|
+
STEP_TIMEOUT_MS: STEP_TIMEOUT_MS,
|
|
15215
|
+
capStream: capStream,
|
|
15216
|
+
escalateWorkflowFailure: escalateWorkflowFailure,
|
|
15217
|
+
getRun: getRun,
|
|
15218
|
+
isWorkflowInFlight: isWorkflowInFlight,
|
|
15219
|
+
listRuns: listRuns,
|
|
15220
|
+
loadRuns: loadRuns,
|
|
15221
|
+
maxRunsPerWorkflow: maxRunsPerWorkflow,
|
|
15222
|
+
reconcileInterruptedRuns: reconcileInterruptedRuns,
|
|
15223
|
+
recordRun: recordRun,
|
|
15224
|
+
runWorkflow: runWorkflow,
|
|
15225
|
+
runsDir: runsDir,
|
|
15226
|
+
stepTimeoutMs: stepTimeoutMs
|
|
15227
|
+
});
|
|
15228
|
+
|
|
14763
15229
|
const DEFAULT_IDLE_COOLDOWN_MS = 15e3;
|
|
14764
15230
|
function idleCooldownMs() {
|
|
14765
15231
|
const raw = Number(process.env.SVAMP_WORKFLOW_IDLE_COOLDOWN_MS);
|
|
@@ -15162,7 +15628,9 @@ async function readCredentials() {
|
|
|
15162
15628
|
}
|
|
15163
15629
|
async function writeCredentials(creds) {
|
|
15164
15630
|
const path = getCredentialsPath();
|
|
15165
|
-
|
|
15631
|
+
const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
15632
|
+
await writeFile(tmp, JSON.stringify(creds), { mode: 384 });
|
|
15633
|
+
await rename$1(tmp, path);
|
|
15166
15634
|
}
|
|
15167
15635
|
async function checkAndRefreshOAuthToken(force = false, logger) {
|
|
15168
15636
|
const log = logger?.log ?? (() => {
|
|
@@ -17051,7 +17519,7 @@ async function startDaemon(options) {
|
|
|
17051
17519
|
try {
|
|
17052
17520
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
17053
17521
|
if (!dir) return;
|
|
17054
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
17522
|
+
const { reconcileServiceLinks } = await import('./agentCommands-D68wBf-y.mjs');
|
|
17055
17523
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
17056
17524
|
const config = readSvampConfig(configPath);
|
|
17057
17525
|
const entries = Array.from(urls.entries());
|
|
@@ -17069,7 +17537,7 @@ async function startDaemon(options) {
|
|
|
17069
17537
|
}
|
|
17070
17538
|
}
|
|
17071
17539
|
async function createExposedTunnel(spec) {
|
|
17072
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
17540
|
+
const { FrpcTunnel } = await import('./frpc-NDcBtpwa.mjs');
|
|
17073
17541
|
const tunnel = new FrpcTunnel({
|
|
17074
17542
|
name: spec.name,
|
|
17075
17543
|
ports: spec.ports,
|
|
@@ -17138,10 +17606,14 @@ async function startDaemon(options) {
|
|
|
17138
17606
|
const routeSupervisionVerdict = (parentId, v) => {
|
|
17139
17607
|
try {
|
|
17140
17608
|
if (!parentId) return;
|
|
17141
|
-
const
|
|
17142
|
-
const
|
|
17143
|
-
|
|
17144
|
-
|
|
17609
|
+
const tracked = Array.from(pidToTrackedSession.values());
|
|
17610
|
+
const parent = tracked.find((t) => t.svampSessionId === parentId);
|
|
17611
|
+
const liveParent = tracked.find((t) => t.svampSessionId === parentId && !t.stopped);
|
|
17612
|
+
const send = liveParent?.sessionRPCHandlers?.sendInboxMessage;
|
|
17613
|
+
if (!send) {
|
|
17614
|
+
logger.log(`[supervision] parent ${parentId.slice(0, 8)} not live \u2014 recording verdict on the backlog issue only (no in-memory wake)`);
|
|
17615
|
+
}
|
|
17616
|
+
if (send) Promise.resolve(send({
|
|
17145
17617
|
messageId: randomUUID$1(),
|
|
17146
17618
|
from: `agent:${v.sessionId}`,
|
|
17147
17619
|
fromSession: v.sessionId,
|
|
@@ -17680,24 +18152,22 @@ ${parts.join("\n")}`);
|
|
|
17680
18152
|
const STUCK_CHECK_INTERVAL_MS = 2 * 60 * 1e3;
|
|
17681
18153
|
let lastOutputTime = Date.now();
|
|
17682
18154
|
let stuckWatchdogTimer = null;
|
|
17683
|
-
const hasActiveChildren = (pid) => {
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
return false;
|
|
17689
|
-
}
|
|
17690
|
-
};
|
|
18155
|
+
const hasActiveChildren = (pid) => new Promise((resolve2) => {
|
|
18156
|
+
execFile("pgrep", ["-P", String(pid)], { encoding: "utf8", timeout: 5e3 }, (err, stdout) => {
|
|
18157
|
+
resolve2(!err && String(stdout).trim().length > 0);
|
|
18158
|
+
});
|
|
18159
|
+
});
|
|
17691
18160
|
const startStuckWatchdog = () => {
|
|
17692
18161
|
if (stuckWatchdogTimer) return;
|
|
17693
|
-
stuckWatchdogTimer = setInterval(() => {
|
|
18162
|
+
stuckWatchdogTimer = setInterval(async () => {
|
|
17694
18163
|
if (!claudeProcess || claudeProcess.exitCode !== null) return;
|
|
17695
18164
|
if (!sessionWasProcessing) return;
|
|
17696
18165
|
if (!isLoopActive(directory, sessionId)) return;
|
|
17697
|
-
if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
|
|
18166
|
+
if (claudeProcess.pid && await hasActiveChildren(claudeProcess.pid)) {
|
|
17698
18167
|
lastOutputTime = Date.now();
|
|
17699
18168
|
return;
|
|
17700
18169
|
}
|
|
18170
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) return;
|
|
17701
18171
|
const elapsed = Date.now() - lastOutputTime;
|
|
17702
18172
|
if (elapsed > STUCK_PROCESS_TIMEOUT_MS) {
|
|
17703
18173
|
logger.log(`[Session ${sessionId}] Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to resume the loop`);
|
|
@@ -18398,7 +18868,7 @@ ${parts.join("\n")}`);
|
|
|
18398
18868
|
auto_resumes: prog.auto_resumes
|
|
18399
18869
|
});
|
|
18400
18870
|
const extNote = summarizeExtensions(ls.extensions, maxExt);
|
|
18401
|
-
const resumeHint = iterStop ? `Extend with a reason: svamp session loop-extend ${sessionId} --reason "<why>" \xB7 or set a new cap: svamp session loop-resume ${sessionId} --max <N>` : `
|
|
18871
|
+
const resumeHint = iterStop ? `Extend with a reason: svamp session loop-extend ${sessionId} --reason "<why>" \xB7 or set a new cap: svamp session loop-resume ${sessionId} --max <N>` : `Raise the cost cap to resume (--max won't \u2014 it only raises the iteration cap): svamp session loop ${sessionId} --max-runtime-sec <N> and/or --max-tokens-per-hour <N>`;
|
|
18402
18872
|
const detail = `${budgetCheck.reason} (hard ${iterStop ? "iteration ceiling" : "cost ceiling"} \u2014 will not auto-resume)${extNote ? ` [${extNote}]` : ""}. ${resumeHint}`;
|
|
18403
18873
|
sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${detail}`, level: "warning" }, "event");
|
|
18404
18874
|
checkSvampConfig?.();
|
|
@@ -19285,19 +19755,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19285
19755
|
});
|
|
19286
19756
|
},
|
|
19287
19757
|
onIssue: async (params) => {
|
|
19288
|
-
const { issueRpc } = await import('./rpc
|
|
19758
|
+
const { issueRpc } = await import('./rpc-K5xzddMF.mjs');
|
|
19289
19759
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
19290
19760
|
},
|
|
19291
19761
|
onWorkflow: async (params) => {
|
|
19292
|
-
const { workflowRpc } = await import('./rpc-
|
|
19762
|
+
const { workflowRpc } = await import('./rpc-DSyBSuTF.mjs');
|
|
19293
19763
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
19294
19764
|
},
|
|
19295
19765
|
onRipgrep: async (args, cwd) => {
|
|
19296
|
-
const { execFile } = await import('child_process');
|
|
19766
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
19297
19767
|
const rgCwd = cwd || directory;
|
|
19298
19768
|
const argv = Array.isArray(args) ? args : String(args).split(/\s+/).filter(Boolean);
|
|
19299
19769
|
return new Promise((resolve2, reject) => {
|
|
19300
|
-
|
|
19770
|
+
execFile2("rg", argv, { cwd: rgCwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
|
|
19301
19771
|
if (err && !stdout) {
|
|
19302
19772
|
reject(new Error(err.message));
|
|
19303
19773
|
} else {
|
|
@@ -19921,19 +20391,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19921
20391
|
});
|
|
19922
20392
|
},
|
|
19923
20393
|
onIssue: async (params) => {
|
|
19924
|
-
const { issueRpc } = await import('./rpc
|
|
20394
|
+
const { issueRpc } = await import('./rpc-K5xzddMF.mjs');
|
|
19925
20395
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
19926
20396
|
},
|
|
19927
20397
|
onWorkflow: async (params) => {
|
|
19928
|
-
const { workflowRpc } = await import('./rpc-
|
|
20398
|
+
const { workflowRpc } = await import('./rpc-DSyBSuTF.mjs');
|
|
19929
20399
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
19930
20400
|
},
|
|
19931
20401
|
onRipgrep: async (args, cwd) => {
|
|
19932
|
-
const { execFile } = await import('child_process');
|
|
20402
|
+
const { execFile: execFile2 } = await import('child_process');
|
|
19933
20403
|
const rgCwd = cwd || directory;
|
|
19934
20404
|
const argv = Array.isArray(args) ? args : String(args).split(/\s+/).filter(Boolean);
|
|
19935
20405
|
return new Promise((resolve2, reject) => {
|
|
19936
|
-
|
|
20406
|
+
execFile2("rg", argv, { cwd: rgCwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
|
|
19937
20407
|
if (err && !stdout) {
|
|
19938
20408
|
reject(new Error(err.message));
|
|
19939
20409
|
} else {
|
|
@@ -20208,7 +20678,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20208
20678
|
const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
|
|
20209
20679
|
const oracleCmd = typeof ls.oracle === "string" && ls.oracle.trim() ? ls.oracle.trim() : void 0;
|
|
20210
20680
|
const acpCumTokens = agentBackend.getCumulativeTokens?.() ?? 0;
|
|
20211
|
-
|
|
20681
|
+
let acpPrevCum = typeof ls.acp_last_cum_tokens === "number" ? ls.acp_last_cum_tokens : 0;
|
|
20682
|
+
if (acpCumTokens < acpPrevCum) acpPrevCum = 0;
|
|
20212
20683
|
const acpDeltaTokens = Math.max(0, acpCumTokens - acpPrevCum);
|
|
20213
20684
|
ls.acp_last_cum_tokens = acpCumTokens;
|
|
20214
20685
|
const ledger = accumulateLedger(ls.ledger, { turns: 1, tokens: acpDeltaTokens, ts: now });
|
|
@@ -20242,7 +20713,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20242
20713
|
const iterStop = !isCostCeiling(budgetCheck.kind);
|
|
20243
20714
|
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: now, gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`, ledger });
|
|
20244
20715
|
const acpExtNote = summarizeExtensions(ls.extensions, acpMaxExt);
|
|
20245
|
-
const acpResumeHint = iterStop ? `Extend with a reason: svamp session loop-extend ${sessionId} --reason "<why>" \xB7 or set a new cap: svamp session loop-resume ${sessionId} --max <N>` : `
|
|
20716
|
+
const acpResumeHint = iterStop ? `Extend with a reason: svamp session loop-extend ${sessionId} --reason "<why>" \xB7 or set a new cap: svamp session loop-resume ${sessionId} --max <N>` : `Raise the cost cap to resume (--max won't \u2014 it only raises the iteration cap): svamp session loop ${sessionId} --max-runtime-sec <N> and/or --max-tokens-per-hour <N>`;
|
|
20246
20717
|
sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason} (hard ${iterStop ? "iteration ceiling" : "cost ceiling"} \u2014 will not auto-resume)${acpExtNote ? ` [${acpExtNote}]` : ""}. ${acpResumeHint}`, level: "warning" }, "event");
|
|
20247
20718
|
checkSvampConfig?.();
|
|
20248
20719
|
return;
|
|
@@ -20750,7 +21221,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
20750
21221
|
const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
|
|
20751
21222
|
if (channelHttpPort > 0) {
|
|
20752
21223
|
try {
|
|
20753
|
-
const { createChannelHttpServer } = await import('./httpServer-
|
|
21224
|
+
const { createChannelHttpServer } = await import('./httpServer-1XjB2h3K.mjs');
|
|
20754
21225
|
const channelHttpServer = createChannelHttpServer({
|
|
20755
21226
|
getSessionIds: () => {
|
|
20756
21227
|
const ids = [];
|
|
@@ -20991,7 +21462,10 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
20991
21462
|
}
|
|
20992
21463
|
if (sessionsToAutoContinue.length > 0 && !options?.noAutoContinue) {
|
|
20993
21464
|
logger.log(`Auto-continuing ${sessionsToAutoContinue.length} interrupted session(s)...`);
|
|
20994
|
-
|
|
21465
|
+
const AUTO_CONTINUE_STEP_MS = Number(process.env.SVAMP_AUTO_CONTINUE_STEP_MS) || 300;
|
|
21466
|
+
const AUTO_CONTINUE_MAX_SPREAD_MS = Number(process.env.SVAMP_AUTO_CONTINUE_MAX_SPREAD_MS) || 6e4;
|
|
21467
|
+
sessionsToAutoContinue.forEach((sessionId, i) => {
|
|
21468
|
+
const delay = 2e3 + Math.min(i * AUTO_CONTINUE_STEP_MS, AUTO_CONTINUE_MAX_SPREAD_MS) + Math.floor(Math.random() * AUTO_CONTINUE_STEP_MS);
|
|
20995
21469
|
setTimeout(async () => {
|
|
20996
21470
|
try {
|
|
20997
21471
|
const tracked = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId);
|
|
@@ -21008,8 +21482,8 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21008
21482
|
} catch (err) {
|
|
21009
21483
|
logger.log(`Failed to auto-continue session ${sessionId}: ${err.message}`);
|
|
21010
21484
|
}
|
|
21011
|
-
},
|
|
21012
|
-
}
|
|
21485
|
+
}, delay);
|
|
21486
|
+
});
|
|
21013
21487
|
} else if (sessionsToAutoContinue.length > 0) {
|
|
21014
21488
|
logger.log(`Skipping auto-continue for ${sessionsToAutoContinue.length} interrupted session(s) (--no-auto-continue)`);
|
|
21015
21489
|
}
|
|
@@ -21072,15 +21546,24 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21072
21546
|
const PING_TIMEOUT_MS = 15e3;
|
|
21073
21547
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
21074
21548
|
const RECONNECT_JITTER_MS = 2500;
|
|
21075
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
21549
|
+
const { WorkflowScheduler } = await import('./scheduler-CtKlSyIq.mjs');
|
|
21550
|
+
const workflowProjectRoots = () => {
|
|
21551
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
21552
|
+
for (const s of pidToTrackedSession.values()) {
|
|
21553
|
+
if (!s.stopped && s.directory) dirs.add(s.directory);
|
|
21554
|
+
}
|
|
21555
|
+
return [...dirs];
|
|
21556
|
+
};
|
|
21557
|
+
try {
|
|
21558
|
+
const { reconcileInterruptedRuns } = await Promise.resolve().then(function () { return runStore; });
|
|
21559
|
+
let reconciled = 0;
|
|
21560
|
+
for (const root of workflowProjectRoots()) reconciled += reconcileInterruptedRuns(root);
|
|
21561
|
+
if (reconciled > 0) logger.log(`[workflow] reconciled ${reconciled} interrupted run(s) (running \u2192 failed) after restart`);
|
|
21562
|
+
} catch (e) {
|
|
21563
|
+
logger.log(`[workflow] interrupted-run reconcile skipped: ${e?.message || e}`);
|
|
21564
|
+
}
|
|
21076
21565
|
const workflowScheduler = new WorkflowScheduler({
|
|
21077
|
-
projectRoots:
|
|
21078
|
-
const dirs = /* @__PURE__ */ new Set();
|
|
21079
|
-
for (const s of pidToTrackedSession.values()) {
|
|
21080
|
-
if (!s.stopped && s.directory) dirs.add(s.directory);
|
|
21081
|
-
}
|
|
21082
|
-
return [...dirs];
|
|
21083
|
-
},
|
|
21566
|
+
projectRoots: workflowProjectRoots,
|
|
21084
21567
|
log: (m) => logger.log(m)
|
|
21085
21568
|
});
|
|
21086
21569
|
const workflowSchedulerInterval = setInterval(() => {
|
|
@@ -21696,4 +22179,4 @@ var run = /*#__PURE__*/Object.freeze({
|
|
|
21696
22179
|
writeStopMarker: writeStopMarker
|
|
21697
22180
|
});
|
|
21698
22181
|
|
|
21699
|
-
export {
|
|
22182
|
+
export { downloadSkillFile as $, removeWorkflow as A, validateWorkflowName as B, saveWorkflow as C, rawWorkflow as D, listWorkflows as E, isWorkflowEnabled as F, workflowCrons as G, cronMatches as H, summarize as I, workflowSteps as J, parseJwtEmail as K, computeCollectionConfigUpdate as L, SYSTEM_COLLECTION_CONFIG as M, loadMachineContext as N, buildMachineInstructions as O, machineToolsForRole as P, buildMachineTools as Q, READ_ONLY_TOOLS as R, SharingNotificationSync as S, parseFrontmatter as T, getSkillsServer as U, getSkillsWorkspaceName as V, getSkillsCollectionName as W, fetchWithTimeout as X, searchSkills as Y, SKILLS_DIR as Z, getSkillInfo as _, createSessionStore as a, listSkillFiles as a0, resolveModel as a1, clearStopMarker as a2, stopMarkerExists as a3, formatHandle as a4, normalizeAllowedUser as a5, loadSecurityContextConfig as a6, resolveSecurityContext as a7, buildSecurityContextFromFlags as a8, mergeSecurityContexts as a9, buildSessionShareUrl as aa, computeOutboundHop as ab, registerAwaitingReply as ac, buildMachineShareUrl as ad, parseHandle as ae, handleMatchesMetadata as af, describeMisconfiguration as ag, buildMachineDeps as ah, applyClaudeProxyEnv as ai, composeSessionId as aj, generateFriendlyName as ak, generateHookSettings as al, staticFileServer as am, instanceConfig as an, claudeAuth as ao, codexProvider as ap, projectInfo as aq, DefaultTransport$1 as ar, acpBackend as as, acpAgentConfig as at, codexAppServerBackend as au, GeminiTransport$1 as av, api as aw, run as ax, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, resolveProjectRoot as j, getIssue as k, resumeIssue as l, addComment as m, addIssue as n, listIssues as o, pauseIssue as p, searchIssues as q, registerMachineService as r, startDaemon as s, isVisibleTo as t, updateIssue as u, getRun as v, listRuns as w, getWorkflow as x, runWorkflow as y, setWorkflowEnabled as z };
|