svamp-cli 0.2.325 → 0.2.327
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/README.md +2 -3
- package/bin/skills/crew/SKILL.md +10 -2
- package/dist/{adminCommands-CMSvATDd.mjs → adminCommands-BVXEj5XN.mjs} +1 -1
- package/dist/{agentCommands-BE3bFu8e.mjs → agentCommands-BiSVzb5D.mjs} +5 -5
- package/dist/{auth-BmFYRinf.mjs → auth-D0ji0kh3.mjs} +3 -3
- package/dist/{cli-DiCcPggc.mjs → cli-CQlIsRxF.mjs} +79 -79
- package/dist/cli.mjs +4 -4
- package/dist/{commands-uYZfcxk0.mjs → commands-BkzYDwHO.mjs} +3 -3
- package/dist/{commands-C3Bkw-Km.mjs → commands-C4z-QQdO.mjs} +13 -13
- package/dist/{commands-Coy5CnDf.mjs → commands-CPZdcLvm.mjs} +3 -3
- package/dist/{commands-Z_44kVdm.mjs → commands-CXJmXwQo.mjs} +3 -3
- package/dist/{commands-WDZS1Clb.mjs → commands-Cegv66fj.mjs} +4 -4
- package/dist/{commands-B6fVaVgM.mjs → commands-D5R0AmXt.mjs} +3 -3
- package/dist/{commands-7ExCePUv.mjs → commands-D61SEgo0.mjs} +2 -2
- package/dist/{commands-D4R66JMe.mjs → commands-DexO3t-N.mjs} +1 -1
- package/dist/{fleet-DJj0ns7e.mjs → fleet-CCVC-NiE.mjs} +3 -3
- package/dist/{headlessCli-Bx41HZIx.mjs → headlessCli-CFgDFTPd.mjs} +4 -4
- package/dist/{httpServer-1XjB2h3K.mjs → httpServer-Be1Hcddr.mjs} +59 -38
- package/dist/index.mjs +3 -3
- package/dist/{notifyCommands-BV4ajmhc.mjs → notifyCommands-BfLeyHId.mjs} +1 -1
- package/dist/package-TcR2vlFS.mjs +64 -0
- package/dist/{pinnedClaudeCode-8SZDu7O-.mjs → pinnedClaudeCode-BO0Qs_kE.mjs} +1 -1
- package/dist/{rpc-D_pFjWr1.mjs → rpc-DXY3KqiA.mjs} +3 -3
- package/dist/{rpc-D9LFUW93.mjs → rpc-hdw0ZlCj.mjs} +3 -3
- package/dist/{run-D7G5CDp7.mjs → run-CewXMfIf.mjs} +1 -1
- package/dist/{run-CO4F7ZaJ.mjs → run-DdTcDN3e.mjs} +372 -86
- package/dist/{scheduler-bgEMu7PK.mjs → scheduler-3LUL1jFD.mjs} +3 -3
- package/dist/{serveCommands-MbZuPWD4.mjs → serveCommands-DUJ4eojk.mjs} +12 -12
- package/dist/{sideband-CadJ3gg1.mjs → sideband-BRHu4M46.mjs} +3 -3
- package/package.json +2 -2
- package/dist/package-C3Usc86E.mjs +0 -64
|
@@ -8,15 +8,15 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { execFile, execSync, spawn, exec as exec$1 } from 'child_process';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
10
|
import { randomUUID, createHash } from 'crypto';
|
|
11
|
-
import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
12
11
|
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync as rmSync$1, cpSync, statSync, realpathSync, readdirSync, renameSync, appendFileSync, openSync, readSync, closeSync, unlinkSync as unlinkSync$1, writeSync } from 'node:fs';
|
|
12
|
+
import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
|
|
13
|
+
import { randomUUID as randomUUID$1, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
13
14
|
import { exec, spawn as spawn$1, execSync as execSync$1, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
14
15
|
import { promisify } from 'util';
|
|
15
16
|
import * as http from 'http';
|
|
16
17
|
import * as net from 'net';
|
|
17
18
|
import { createServer } from 'net';
|
|
18
19
|
import os, { homedir as homedir$1, platform as platform$1 } from 'node:os';
|
|
19
|
-
import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
|
|
20
20
|
import { EventEmitter } from 'node:events';
|
|
21
21
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
22
22
|
import { promisify as promisify$1 } from 'node:util';
|
|
@@ -3001,6 +3001,13 @@ function assertNonAdminMountDirSafe(directory, homeDir, resolveReal = defaultRes
|
|
|
3001
3001
|
}
|
|
3002
3002
|
}
|
|
3003
3003
|
}
|
|
3004
|
+
function hiddenSegmentOf(relPath) {
|
|
3005
|
+
for (const seg of relPath.split(/[\\/]+/)) {
|
|
3006
|
+
if (!seg || seg === ".") continue;
|
|
3007
|
+
if (seg.startsWith(".")) return seg;
|
|
3008
|
+
}
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3004
3011
|
function defaultResolveReal(p) {
|
|
3005
3012
|
try {
|
|
3006
3013
|
return fs.realpathSync(p);
|
|
@@ -3160,6 +3167,9 @@ class ServeManager {
|
|
|
3160
3167
|
hyphaServerUrl;
|
|
3161
3168
|
/** #126: daemon-provided lookup of sessionId → { cwd, sharing, ownerEmail } for the files gateway. */
|
|
3162
3169
|
sessionResolver = null;
|
|
3170
|
+
/** Channel-capability authorizer: a `fileAccess`-enabled channel's caller key grants
|
|
3171
|
+
* whole-project file management for its session. Checked BEFORE email auth. */
|
|
3172
|
+
capabilityAuthorizer = null;
|
|
3163
3173
|
/**
|
|
3164
3174
|
* #0944/#0945: daemon-provided "this mount no longer backs that session's Launch Pad entry"
|
|
3165
3175
|
* hook. Fired with the OWNING session id, which is the only correct target — the CLI-side
|
|
@@ -3189,6 +3199,43 @@ class ServeManager {
|
|
|
3189
3199
|
setSessionResolver(fn) {
|
|
3190
3200
|
this.sessionResolver = fn;
|
|
3191
3201
|
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Register the channel-capability authorizer used by the session-files gateway to admit
|
|
3204
|
+
* a `fileAccess`-enabled channel's caller key (ck_… bearer). Call once at daemon start.
|
|
3205
|
+
*/
|
|
3206
|
+
setCapabilityAuthorizer(fn) {
|
|
3207
|
+
this.capabilityAuthorizer = fn;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Extract a channel capability token from the request, if one is present. A channel caller
|
|
3211
|
+
* key is `ck_`-prefixed; we ONLY return a token that carries that prefix so a Hypha JWT
|
|
3212
|
+
* bearer (used by the email-identity path) never collides with the capability authorizer.
|
|
3213
|
+
* Accepted carriers: `Authorization: Bearer ck_…` header (any verb), or `?key=ck_…` query
|
|
3214
|
+
* param (READ verbs only). Returns null when no capability token is presented.
|
|
3215
|
+
* NEVER log the returned value.
|
|
3216
|
+
*
|
|
3217
|
+
* #1047: the query carrier is restricted to GET/HEAD. A query string is a poor place for a
|
|
3218
|
+
* credential — it lands in browser history, in the `Referer` of any HTML the gateway serves,
|
|
3219
|
+
* and in every tunnel/intermediary access log on the path (and the gateway is reached over an
|
|
3220
|
+
* frps tunnel whose logs the user does not control). This is the same exposure class #1006
|
|
3221
|
+
* closed for the Hypha JWT in download URLs. It is kept for reads because a browser-embeddable
|
|
3222
|
+
* URL genuinely needs it (an `<img src>` or a download link cannot set a header), but a leaked
|
|
3223
|
+
* URL must not also confer PUT/DELETE over the whole project. The generated skill already
|
|
3224
|
+
* documents only the Authorization form (see `fileSection` in channel/store.ts), so this
|
|
3225
|
+
* narrows an unused carrier rather than breaking the documented path.
|
|
3226
|
+
*/
|
|
3227
|
+
extractCapabilityToken(req, url) {
|
|
3228
|
+
const auth = req.headers["authorization"];
|
|
3229
|
+
if (typeof auth === "string") {
|
|
3230
|
+
const m = /^Bearer\s+(\S+)$/i.exec(auth.trim());
|
|
3231
|
+
if (m && m[1].startsWith("ck_")) return m[1];
|
|
3232
|
+
}
|
|
3233
|
+
const method = (req.method || "GET").toUpperCase();
|
|
3234
|
+
if (method !== "GET" && method !== "HEAD") return null;
|
|
3235
|
+
const q = url.searchParams.get("key");
|
|
3236
|
+
if (q && q.startsWith("ck_")) return q;
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3192
3239
|
/**
|
|
3193
3240
|
* #0944/#0945: register the daemon's "drop this session's Launch Pad link for this mount"
|
|
3194
3241
|
* callback. Fired when a mount stops backing a session's Pad entry — on a real removal, and
|
|
@@ -3785,40 +3832,59 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3785
3832
|
deny(404, "Unknown session");
|
|
3786
3833
|
return;
|
|
3787
3834
|
}
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3835
|
+
let capabilityGranted = false;
|
|
3836
|
+
const capToken = this.extractCapabilityToken(req, url);
|
|
3837
|
+
if (capToken && this.capabilityAuthorizer) {
|
|
3838
|
+
try {
|
|
3839
|
+
capabilityGranted = this.capabilityAuthorizer(sessionId, capToken);
|
|
3840
|
+
} catch {
|
|
3841
|
+
capabilityGranted = false;
|
|
3842
|
+
}
|
|
3792
3843
|
}
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
const sharing = target.sharing;
|
|
3799
|
-
if (sharing && sharing.enabled) {
|
|
3800
|
-
try {
|
|
3801
|
-
authorizeRequest(principal, sharing, "admin");
|
|
3802
|
-
granted = true;
|
|
3803
|
-
} catch {
|
|
3804
|
-
}
|
|
3844
|
+
if (!capabilityGranted) {
|
|
3845
|
+
const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
|
|
3846
|
+
if (!userEmail) {
|
|
3847
|
+
deny(401, "Authentication required");
|
|
3848
|
+
return;
|
|
3805
3849
|
}
|
|
3806
|
-
const
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3850
|
+
const ownerEmail = (target.ownerEmail || "").toLowerCase();
|
|
3851
|
+
const isOwner = !!ownerEmail && userEmail.toLowerCase() === ownerEmail;
|
|
3852
|
+
if (!isOwner) {
|
|
3853
|
+
const principal = { user: { email: userEmail } };
|
|
3854
|
+
let granted = false;
|
|
3855
|
+
const sharing = target.sharing;
|
|
3856
|
+
if (sharing && sharing.enabled) {
|
|
3857
|
+
try {
|
|
3858
|
+
authorizeRequest(principal, sharing, "admin");
|
|
3859
|
+
granted = true;
|
|
3860
|
+
} catch {
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
const machineSharing = target.machineSharing;
|
|
3864
|
+
if (!granted && machineSharing && machineSharing.enabled) {
|
|
3865
|
+
try {
|
|
3866
|
+
authorizeRequest(principal, machineSharing, "admin");
|
|
3867
|
+
granted = true;
|
|
3868
|
+
} catch {
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (!granted) {
|
|
3872
|
+
this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and not a session/machine admin`);
|
|
3873
|
+
deny(403, "Access denied");
|
|
3874
|
+
return;
|
|
3812
3875
|
}
|
|
3813
|
-
}
|
|
3814
|
-
if (!granted) {
|
|
3815
|
-
this.log(`Files-gateway DENY '${sessionId}': ${userEmail} not owner and not a session/machine admin`);
|
|
3816
|
-
deny(403, "Access denied");
|
|
3817
|
-
return;
|
|
3818
3876
|
}
|
|
3819
3877
|
}
|
|
3820
3878
|
const rootDir = target.cwd;
|
|
3821
3879
|
const normRel = this.toSessionRelative(rootDir, m[2] || "");
|
|
3880
|
+
if (capabilityGranted) {
|
|
3881
|
+
const badSeg = hiddenSegmentOf(normRel);
|
|
3882
|
+
if (badSeg) {
|
|
3883
|
+
this.log(`Files-gateway DENY '${sessionId}': channel capability may not reach a hidden path (${badSeg})`);
|
|
3884
|
+
deny(403, `Forbidden: channel file access may not reach hidden paths (${badSeg})`);
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3822
3888
|
if (method === "GET" || method === "HEAD") {
|
|
3823
3889
|
if (url.searchParams.get("list") === "1") {
|
|
3824
3890
|
const lex = this.resolveContainedPath(rootDir, normRel);
|
|
@@ -3840,6 +3906,7 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3840
3906
|
}
|
|
3841
3907
|
return { name: d.name, isDir, size, mtime };
|
|
3842
3908
|
});
|
|
3909
|
+
if (capabilityGranted) entries = entries.filter((e) => !e.name.startsWith("."));
|
|
3843
3910
|
} catch (err) {
|
|
3844
3911
|
deny(err?.code === "ENOENT" ? 404 : 500, `Listing failed: ${err?.message || err}`);
|
|
3845
3912
|
return;
|
|
@@ -4420,6 +4487,7 @@ var serveManager = /*#__PURE__*/Object.freeze({
|
|
|
4420
4487
|
ServeManager: ServeManager,
|
|
4421
4488
|
assertNonAdminMountDirSafe: assertNonAdminMountDirSafe,
|
|
4422
4489
|
buildLinkSubdomain: buildLinkSubdomain,
|
|
4490
|
+
hiddenSegmentOf: hiddenSegmentOf,
|
|
4423
4491
|
resolveMountOwnerEmail: resolveMountOwnerEmail,
|
|
4424
4492
|
sanitizeMountForRole: sanitizeMountForRole
|
|
4425
4493
|
});
|
|
@@ -4598,8 +4666,13 @@ function applyPromptCacheTtl(spawnEnv) {
|
|
|
4598
4666
|
const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
|
|
4599
4667
|
if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
|
|
4600
4668
|
}
|
|
4669
|
+
function applyTodoTools(spawnEnv) {
|
|
4670
|
+
const override = spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS ?? process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS;
|
|
4671
|
+
if (override === void 0) spawnEnv.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1";
|
|
4672
|
+
}
|
|
4601
4673
|
function applyClaudeProxyEnv(spawnEnv) {
|
|
4602
4674
|
const mode = currentMode();
|
|
4675
|
+
applyTodoTools(spawnEnv);
|
|
4603
4676
|
if (mode === "hypha") {
|
|
4604
4677
|
const proxyUrl = resolveHyphaProxyUrl();
|
|
4605
4678
|
if (!proxyUrl) {
|
|
@@ -5613,6 +5686,12 @@ function validateChannel(c) {
|
|
|
5613
5686
|
if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
|
|
5614
5687
|
for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
|
|
5615
5688
|
errs.push(...validateUploadConfig(c));
|
|
5689
|
+
if (c.fileAccess !== void 0) {
|
|
5690
|
+
if (typeof c.fileAccess !== "object" || c.fileAccess === null || Array.isArray(c.fileAccess))
|
|
5691
|
+
errs.push("fileAccess must be an object");
|
|
5692
|
+
else if (c.fileAccess.enabled !== void 0 && typeof c.fileAccess.enabled !== "boolean")
|
|
5693
|
+
errs.push("fileAccess.enabled must be a boolean");
|
|
5694
|
+
}
|
|
5616
5695
|
return errs;
|
|
5617
5696
|
}
|
|
5618
5697
|
function normalizeBind(c) {
|
|
@@ -5636,14 +5715,34 @@ function channelAsyncRepliable(c) {
|
|
|
5636
5715
|
if ((c.action?.kind ?? "message") !== "message") return false;
|
|
5637
5716
|
return bindMode(c) !== "stateless";
|
|
5638
5717
|
}
|
|
5718
|
+
function channelKeyGrantsFileAccess(channels, token) {
|
|
5719
|
+
if (!token || !token.startsWith("ck_")) return false;
|
|
5720
|
+
const presented = createHash$1("sha256").update(token).digest();
|
|
5721
|
+
const eq = (candidate) => {
|
|
5722
|
+
if (!candidate) return false;
|
|
5723
|
+
try {
|
|
5724
|
+
return timingSafeEqual(presented, createHash$1("sha256").update(candidate).digest());
|
|
5725
|
+
} catch {
|
|
5726
|
+
return false;
|
|
5727
|
+
}
|
|
5728
|
+
};
|
|
5729
|
+
for (const ch of channels) {
|
|
5730
|
+
if (ch.enabled === false) continue;
|
|
5731
|
+
if (!ch.fileAccess?.enabled) continue;
|
|
5732
|
+
if (eq(ch.identity?.shared_key)) return true;
|
|
5733
|
+
for (const caller of ch.identity?.callers || []) if (eq(caller.key)) return true;
|
|
5734
|
+
}
|
|
5735
|
+
return false;
|
|
5736
|
+
}
|
|
5737
|
+
const _channelListCache = /* @__PURE__ */ new Map();
|
|
5738
|
+
const _CHANNEL_CACHE_TTL_MS = 1e3;
|
|
5739
|
+
function invalidateChannelCache(dir) {
|
|
5740
|
+
_channelListCache.delete(dir);
|
|
5741
|
+
}
|
|
5639
5742
|
class ChannelStore {
|
|
5640
5743
|
dir;
|
|
5641
5744
|
constructor(projectDir) {
|
|
5642
5745
|
this.dir = join$1(projectDir, ".svamp", "channels");
|
|
5643
|
-
try {
|
|
5644
|
-
mkdirSync$1(this.dir, { recursive: true });
|
|
5645
|
-
} catch {
|
|
5646
|
-
}
|
|
5647
5746
|
}
|
|
5648
5747
|
// #0982: reject a traversing/oversized id at the path boundary itself, so no
|
|
5649
5748
|
// caller (RPC, HTTP, CLI) can reach outside this.dir regardless of its own checks.
|
|
@@ -5664,19 +5763,43 @@ class ChannelStore {
|
|
|
5664
5763
|
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
5665
5764
|
mkdirSync$1(this.dir, { recursive: true });
|
|
5666
5765
|
const tmp = `${this._path(c.id)}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
5667
|
-
writeFileSync$1(tmp, JSON.stringify(c, null, 2));
|
|
5766
|
+
writeFileSync$1(tmp, JSON.stringify(c, null, 2), { mode: 384 });
|
|
5668
5767
|
renameSync(tmp, this._path(c.id));
|
|
5768
|
+
invalidateChannelCache(this.dir);
|
|
5669
5769
|
return c;
|
|
5670
5770
|
}
|
|
5771
|
+
/**
|
|
5772
|
+
* All channels in this project, parsed. Cached per directory and revalidated with ONE statSync
|
|
5773
|
+
* — see the #1049/#1068 note above for why the uncached version was a daemon-wide stall.
|
|
5774
|
+
*/
|
|
5671
5775
|
list() {
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
}
|
|
5776
|
+
let st;
|
|
5777
|
+
try {
|
|
5778
|
+
const s = statSync(this.dir);
|
|
5779
|
+
st = { mtimeMs: s.mtimeMs, size: s.size };
|
|
5780
|
+
} catch {
|
|
5781
|
+
_channelListCache.delete(this.dir);
|
|
5782
|
+
return [];
|
|
5783
|
+
}
|
|
5784
|
+
const hit = _channelListCache.get(this.dir);
|
|
5785
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size && Date.now() - hit.at < _CHANNEL_CACHE_TTL_MS) {
|
|
5786
|
+
return hit.channels;
|
|
5787
|
+
}
|
|
5788
|
+
let channels;
|
|
5789
|
+
try {
|
|
5790
|
+
channels = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
5791
|
+
try {
|
|
5792
|
+
return normalizeBind(JSON.parse(readFileSync(join$1(this.dir, f), "utf8")));
|
|
5793
|
+
} catch {
|
|
5794
|
+
return null;
|
|
5795
|
+
}
|
|
5796
|
+
}).filter((c) => !!c);
|
|
5797
|
+
} catch {
|
|
5798
|
+
_channelListCache.delete(this.dir);
|
|
5799
|
+
return [];
|
|
5800
|
+
}
|
|
5801
|
+
_channelListCache.set(this.dir, { mtimeMs: st.mtimeMs, size: st.size, at: Date.now(), channels });
|
|
5802
|
+
return channels;
|
|
5680
5803
|
}
|
|
5681
5804
|
get(id) {
|
|
5682
5805
|
try {
|
|
@@ -5694,6 +5817,7 @@ class ChannelStore {
|
|
|
5694
5817
|
const p = this._path(id);
|
|
5695
5818
|
if (existsSync(p)) {
|
|
5696
5819
|
rmSync$1(p);
|
|
5820
|
+
invalidateChannelCache(this.dir);
|
|
5697
5821
|
return true;
|
|
5698
5822
|
}
|
|
5699
5823
|
return false;
|
|
@@ -5734,6 +5858,28 @@ class ChannelStore {
|
|
|
5734
5858
|
return caller;
|
|
5735
5859
|
});
|
|
5736
5860
|
}
|
|
5861
|
+
/**
|
|
5862
|
+
* #1036: revoke ONE caller's key without destroying the channel.
|
|
5863
|
+
*
|
|
5864
|
+
* Before this, the only removal primitive was remove(id) — so withdrawing a single leaked key
|
|
5865
|
+
* meant deleting the whole channel and every other caller with it. That is a bad enough trade
|
|
5866
|
+
* that in practice a leaked key just stayed live, which matters much more now that a caller key
|
|
5867
|
+
* can carry whole-project file access (see channelKeyGrantsFileAccess).
|
|
5868
|
+
*
|
|
5869
|
+
* Same per-channel lock as the sibling mutators (#0679), so a concurrent addCaller cannot
|
|
5870
|
+
* resurrect the revoked entry by writing a stale base. Returns true when a caller was removed.
|
|
5871
|
+
*/
|
|
5872
|
+
removeCaller(id, name) {
|
|
5873
|
+
return withFileLock(this._lock(id), () => {
|
|
5874
|
+
const c = this.get(id);
|
|
5875
|
+
if (!c?.identity?.callers) return false;
|
|
5876
|
+
const before = c.identity.callers.length;
|
|
5877
|
+
c.identity.callers = c.identity.callers.filter((x) => x.name !== name);
|
|
5878
|
+
if (c.identity.callers.length === before) return false;
|
|
5879
|
+
this._writeChannel(c);
|
|
5880
|
+
return true;
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5737
5883
|
}
|
|
5738
5884
|
function routingSession(channel, ctx) {
|
|
5739
5885
|
const mode = bindMode(channel);
|
|
@@ -5846,6 +5992,32 @@ curl -X POST "${uploadUrl}" \\
|
|
|
5846
5992
|
-d '{"kwargs": {"channel": "${channel.id}", "filename": "report.pdf", "content_base64": "<base64>", "content_type": "application/pdf", "from": "your-name", "key": "${key}"}}'
|
|
5847
5993
|
\`\`\``;
|
|
5848
5994
|
})() : "";
|
|
5995
|
+
const filesBase = ctx?.filesEndpoint;
|
|
5996
|
+
const fileSection = channel.fileAccess?.enabled && filesBase ? `
|
|
5997
|
+
|
|
5998
|
+
## Managing project files
|
|
5999
|
+
This channel also grants **full file management of the whole project folder** \u2014 list,
|
|
6000
|
+
download, upload, and delete any file in the working directory (this is NOT the sandboxed
|
|
6001
|
+
\`upload\` above). Authenticate every request with your channel key as a Bearer token:
|
|
6002
|
+
|
|
6003
|
+
Base: \`${filesBase}\` \u2014 append a project-relative path (URL-encode as needed).
|
|
6004
|
+
|
|
6005
|
+
- **List a directory:** \`GET ${filesBase}/<dir>/?list=1\` \u2192 JSON of entries
|
|
6006
|
+
- **Download a file:** \`GET ${filesBase}/<path>\`
|
|
6007
|
+
- **Upload / overwrite:** \`PUT ${filesBase}/<path>\` with the raw file bytes as the body
|
|
6008
|
+
- **Delete a file:** \`DELETE ${filesBase}/<path>\`
|
|
6009
|
+
|
|
6010
|
+
\`\`\`bash
|
|
6011
|
+
# list the project root
|
|
6012
|
+
curl -H "Authorization: Bearer ${key}" "${filesBase}/?list=1"
|
|
6013
|
+
# download a file
|
|
6014
|
+
curl -H "Authorization: Bearer ${key}" "${filesBase}/README.md" -o README.md
|
|
6015
|
+
# upload / overwrite a file (raw bytes)
|
|
6016
|
+
curl -X PUT -H "Authorization: Bearer ${key}" --data-binary @local.txt "${filesBase}/notes/local.txt"
|
|
6017
|
+
# delete a file
|
|
6018
|
+
curl -X DELETE -H "Authorization: Bearer ${key}" "${filesBase}/notes/local.txt"
|
|
6019
|
+
\`\`\`
|
|
6020
|
+
> \u26A0\uFE0F Anyone holding this key can read, write, and **delete** any file in this project. Revoke it when you're done.` : "";
|
|
5849
6021
|
return `---
|
|
5850
6022
|
name: ${name}
|
|
5851
6023
|
description: ${desc}
|
|
@@ -5878,7 +6050,7 @@ curl -X POST "${sendUrl}" \\
|
|
|
5878
6050
|
-d '{"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}'
|
|
5879
6051
|
\`\`\`
|
|
5880
6052
|
|
|
5881
|
-
Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${queueSection}`;
|
|
6053
|
+
Always-current copy of this skill (all ids filled in): ${skillUrl}${uploadSection}${fileSection}${queueSection}`;
|
|
5882
6054
|
}
|
|
5883
6055
|
|
|
5884
6056
|
function keysMatch(a, b) {
|
|
@@ -6347,6 +6519,9 @@ function getParamNames(fn) {
|
|
|
6347
6519
|
const terminalSessions = /* @__PURE__ */ new Map();
|
|
6348
6520
|
let ptyModule = null;
|
|
6349
6521
|
const TERMINAL_IDLE_MS = 30 * 60 * 1e3;
|
|
6522
|
+
const TERMINAL_CLIENT_ABSENT_MS = 4 * 60 * 60 * 1e3;
|
|
6523
|
+
const TERMINAL_BUFFER_MAX_BYTES = 1024 * 1024;
|
|
6524
|
+
const TERMINAL_MAX_SESSIONS = 64;
|
|
6350
6525
|
let terminalReaper = null;
|
|
6351
6526
|
function ensureTerminalReaper() {
|
|
6352
6527
|
if (terminalReaper) return;
|
|
@@ -6354,7 +6529,8 @@ function ensureTerminalReaper() {
|
|
|
6354
6529
|
const now = Date.now();
|
|
6355
6530
|
for (const [id, s] of terminalSessions) {
|
|
6356
6531
|
if (s.exited) continue;
|
|
6357
|
-
|
|
6532
|
+
const abandoned = now - (s.lastClientActivity ?? s.lastActivity) > TERMINAL_CLIENT_ABSENT_MS;
|
|
6533
|
+
if (now - s.lastActivity > TERMINAL_IDLE_MS || abandoned) {
|
|
6358
6534
|
try {
|
|
6359
6535
|
s.pty.kill();
|
|
6360
6536
|
} catch {
|
|
@@ -7208,8 +7384,29 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7208
7384
|
rows,
|
|
7209
7385
|
createdAt: Date.now(),
|
|
7210
7386
|
lastActivity: Date.now(),
|
|
7387
|
+
lastClientActivity: Date.now(),
|
|
7388
|
+
outputBytes: 0,
|
|
7211
7389
|
cwd
|
|
7212
7390
|
};
|
|
7391
|
+
if (terminalSessions.size >= TERMINAL_MAX_SESSIONS) {
|
|
7392
|
+
let victim = null, oldest = Infinity;
|
|
7393
|
+
for (const [id, s] of terminalSessions) {
|
|
7394
|
+
const seen = s.lastClientActivity ?? s.lastActivity;
|
|
7395
|
+
if (seen < oldest) {
|
|
7396
|
+
oldest = seen;
|
|
7397
|
+
victim = id;
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
if (victim) {
|
|
7401
|
+
const v = terminalSessions.get(victim);
|
|
7402
|
+
try {
|
|
7403
|
+
v?.pty.kill();
|
|
7404
|
+
} catch {
|
|
7405
|
+
}
|
|
7406
|
+
terminalSessions.delete(victim);
|
|
7407
|
+
console.log(`[terminal] evicted idlest session ${victim} \u2014 at the ${TERMINAL_MAX_SESSIONS}-PTY ceiling`);
|
|
7408
|
+
}
|
|
7409
|
+
}
|
|
7213
7410
|
terminalSessions.set(sessionId, session);
|
|
7214
7411
|
ensureTerminalReaper();
|
|
7215
7412
|
let outputBatch = "";
|
|
@@ -7220,8 +7417,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7220
7417
|
const batch = outputBatch;
|
|
7221
7418
|
outputBatch = "";
|
|
7222
7419
|
session.outputBuffer.push(batch);
|
|
7223
|
-
|
|
7224
|
-
|
|
7420
|
+
session.outputBytes += batch.length;
|
|
7421
|
+
while (session.outputBytes > TERMINAL_BUFFER_MAX_BYTES && session.outputBuffer.length > 1) {
|
|
7422
|
+
session.outputBytes -= (session.outputBuffer.shift() || "").length;
|
|
7225
7423
|
}
|
|
7226
7424
|
if (terminalClient) {
|
|
7227
7425
|
try {
|
|
@@ -7270,6 +7468,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7270
7468
|
const filtered = filterTerminalResponses(params.data);
|
|
7271
7469
|
if (filtered) session.pty.write(filtered);
|
|
7272
7470
|
session.lastActivity = Date.now();
|
|
7471
|
+
session.lastClientActivity = Date.now();
|
|
7273
7472
|
return { success: true };
|
|
7274
7473
|
},
|
|
7275
7474
|
/** Resize a terminal session. */
|
|
@@ -7291,7 +7490,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
7291
7490
|
const session = terminalSessions.get(params.sessionId);
|
|
7292
7491
|
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
7293
7492
|
const output = session.outputBuffer.splice(0).join("");
|
|
7493
|
+
session.outputBytes = 0;
|
|
7294
7494
|
session.lastActivity = Date.now();
|
|
7495
|
+
session.lastClientActivity = Date.now();
|
|
7295
7496
|
const result = {
|
|
7296
7497
|
output,
|
|
7297
7498
|
exited: session.exited
|
|
@@ -8101,7 +8302,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
8101
8302
|
}
|
|
8102
8303
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
8103
8304
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
8104
|
-
const { toolsForRole } = await import('./sideband-
|
|
8305
|
+
const { toolsForRole } = await import('./sideband-BRHu4M46.mjs');
|
|
8105
8306
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
8106
8307
|
return fmt(r2);
|
|
8107
8308
|
}
|
|
@@ -8206,7 +8407,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
8206
8407
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
8207
8408
|
}
|
|
8208
8409
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
8209
|
-
const { queryCore } = await import('./commands-
|
|
8410
|
+
const { queryCore } = await import('./commands-DexO3t-N.mjs');
|
|
8210
8411
|
const timeout = c.reply?.timeout_sec || 120;
|
|
8211
8412
|
let result;
|
|
8212
8413
|
try {
|
|
@@ -9354,6 +9555,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
|
9354
9555
|
}
|
|
9355
9556
|
}
|
|
9356
9557
|
const messageCountCache = /* @__PURE__ */ new Map();
|
|
9558
|
+
const MESSAGE_COUNT_CACHE_MAX = 2e3;
|
|
9357
9559
|
function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
9358
9560
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
9359
9561
|
if (!existsSync(filePath)) return fallbackInMemory;
|
|
@@ -9381,7 +9583,13 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
|
9381
9583
|
for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
|
|
9382
9584
|
if (data.length > 0 && data[data.length - 1] !== "\n") count++;
|
|
9383
9585
|
}
|
|
9586
|
+
messageCountCache.delete(filePath);
|
|
9384
9587
|
messageCountCache.set(filePath, { size: st.size, mtimeMs: st.mtimeMs, count });
|
|
9588
|
+
while (messageCountCache.size > MESSAGE_COUNT_CACHE_MAX) {
|
|
9589
|
+
const oldest = messageCountCache.keys().next();
|
|
9590
|
+
if (oldest.done) break;
|
|
9591
|
+
messageCountCache.delete(oldest.value);
|
|
9592
|
+
}
|
|
9385
9593
|
return count;
|
|
9386
9594
|
} catch {
|
|
9387
9595
|
return fallbackInMemory;
|
|
@@ -9674,7 +9882,11 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9674
9882
|
key: c.identity?.shared_key,
|
|
9675
9883
|
// The session this skill is being copied FROM — baked into `dynamic` channel
|
|
9676
9884
|
// instructions as the routing target so the copied link lands in THIS session.
|
|
9677
|
-
session: sessionId
|
|
9885
|
+
session: sessionId,
|
|
9886
|
+
// For a fileAccess-enabled channel, the session-files REST gateway prefix
|
|
9887
|
+
// (`<base>/s/<sessionId>/`) so generateSkillBody can render the file curl block.
|
|
9888
|
+
// Null until the always-on gateway tunnel is up ⇒ the file section is omitted.
|
|
9889
|
+
filesEndpoint: callbacks.onFilesEndpoint?.() ?? void 0
|
|
9678
9890
|
});
|
|
9679
9891
|
const skillUrlsFor = (c) => {
|
|
9680
9892
|
if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
|
|
@@ -10250,10 +10462,17 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
10250
10462
|
return { error: e?.message || String(e) };
|
|
10251
10463
|
}
|
|
10252
10464
|
},
|
|
10253
|
-
recordMeetingActivity: async (params) => {
|
|
10465
|
+
recordMeetingActivity: async (params, context) => {
|
|
10254
10466
|
const c = channelStore.get(params.channel);
|
|
10255
10467
|
if (!c || c.enabled === false) return { error: "channel not found" };
|
|
10256
|
-
const
|
|
10468
|
+
const u = context?.user;
|
|
10469
|
+
const r = resolveSender(c, {
|
|
10470
|
+
key: params.key,
|
|
10471
|
+
from: params.from,
|
|
10472
|
+
hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
|
|
10473
|
+
hyphaAnonymous: u?.is_anonymous === true,
|
|
10474
|
+
hyphaWorkspace: u?.scope?.current_workspace
|
|
10475
|
+
});
|
|
10257
10476
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
10258
10477
|
const text = String(params.text || "").replace(/\s+/g, " ").trim().slice(0, 1e3);
|
|
10259
10478
|
if (!text) return { ok: true };
|
|
@@ -10721,7 +10940,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
10721
10940
|
// ── Listener Registration ──
|
|
10722
10941
|
registerListener: async (callback, context) => {
|
|
10723
10942
|
authorizeRequest(context, metadata.sharing, "view");
|
|
10943
|
+
const MAX_LISTENERS = 64;
|
|
10724
10944
|
listeners.push(callback);
|
|
10945
|
+
while (listeners.length > MAX_LISTENERS) listeners.shift();
|
|
10725
10946
|
const replayMessages = messages.slice(-50);
|
|
10726
10947
|
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
10727
10948
|
for (const msg of replayMessages) {
|
|
@@ -11463,6 +11684,25 @@ function handleClaudeMessage(ctx, msg) {
|
|
|
11463
11684
|
}
|
|
11464
11685
|
}
|
|
11465
11686
|
|
|
11687
|
+
function sessionQueueMax(env = process.env) {
|
|
11688
|
+
return Math.max(10, Number(env.SVAMP_SESSION_QUEUE_MAX) || 200);
|
|
11689
|
+
}
|
|
11690
|
+
function appendToMessageQueue(existing, entry, max = sessionQueueMax()) {
|
|
11691
|
+
const next = [...existing, entry];
|
|
11692
|
+
if (next.length <= max) return { queue: next, dropped: 0 };
|
|
11693
|
+
const overflow = next.length - max;
|
|
11694
|
+
let budget = overflow;
|
|
11695
|
+
const kept = [];
|
|
11696
|
+
for (const item of next) {
|
|
11697
|
+
if (budget > 0 && item !== entry && item.inboxHop !== void 0) {
|
|
11698
|
+
budget--;
|
|
11699
|
+
continue;
|
|
11700
|
+
}
|
|
11701
|
+
kept.push(item);
|
|
11702
|
+
}
|
|
11703
|
+
return { queue: kept, dropped: overflow - budget };
|
|
11704
|
+
}
|
|
11705
|
+
|
|
11466
11706
|
const KNOWN_AGENT_LAUNCHERS = ["claude", "codex", "gemini", "kimi"];
|
|
11467
11707
|
function agentLauncherExists(cmd, env = process.env) {
|
|
11468
11708
|
if (!cmd) return false;
|
|
@@ -13580,6 +13820,25 @@ var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
|
13580
13820
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
13581
13821
|
});
|
|
13582
13822
|
|
|
13823
|
+
const COMPLETED_REQUESTS_MAX = 500;
|
|
13824
|
+
function recordCompletedRequest(existing, id, entry, max = COMPLETED_REQUESTS_MAX) {
|
|
13825
|
+
const out = { ...existing || {} };
|
|
13826
|
+
delete out[id];
|
|
13827
|
+
const slim = {
|
|
13828
|
+
completedAt: typeof entry.completedAt === "number" ? entry.completedAt : Date.now(),
|
|
13829
|
+
status: String(entry.status ?? "")
|
|
13830
|
+
};
|
|
13831
|
+
if (typeof entry.tool === "string") slim.tool = entry.tool;
|
|
13832
|
+
if (entry.reason !== void 0) slim.reason = entry.reason;
|
|
13833
|
+
if (entry.mode !== void 0) slim.mode = entry.mode;
|
|
13834
|
+
if (entry.decision !== void 0) slim.decision = entry.decision;
|
|
13835
|
+
if (Array.isArray(entry.allowedTools)) slim.allowedTools = entry.allowedTools;
|
|
13836
|
+
out[id] = slim;
|
|
13837
|
+
const keys = Object.keys(out);
|
|
13838
|
+
for (let i = 0; i < keys.length - max; i++) delete out[keys[i]];
|
|
13839
|
+
return out;
|
|
13840
|
+
}
|
|
13841
|
+
|
|
13583
13842
|
function applyPermissionResolution(sessionService, requestId, approved) {
|
|
13584
13843
|
const reqs = { ...sessionService._agentState?.requests };
|
|
13585
13844
|
if (!(requestId in reqs)) return;
|
|
@@ -13589,14 +13848,12 @@ function applyPermissionResolution(sessionService, requestId, approved) {
|
|
|
13589
13848
|
sessionService.updateAgentState({
|
|
13590
13849
|
controlledByUser: false,
|
|
13591
13850
|
requests: reqs,
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
}
|
|
13599
|
-
}
|
|
13851
|
+
// #1041: bounded + slimmed, same as the Claude permission path in daemon/run.ts.
|
|
13852
|
+
completedRequests: recordCompletedRequest(completedReqs, requestId, {
|
|
13853
|
+
...existingReq || {},
|
|
13854
|
+
completedAt: Date.now(),
|
|
13855
|
+
status: approved ? "approved" : "denied"
|
|
13856
|
+
})
|
|
13600
13857
|
});
|
|
13601
13858
|
}
|
|
13602
13859
|
function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, log, onTurnEnd, getModelLabel) {
|
|
@@ -17917,6 +18174,9 @@ function shouldRunZombieProbe(args) {
|
|
|
17917
18174
|
function nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures) {
|
|
17918
18175
|
return consecutiveHeartbeatFailures + 1;
|
|
17919
18176
|
}
|
|
18177
|
+
function shouldRunServiceProbe(args) {
|
|
18178
|
+
return !args.inGrace && !args.zombieProbeRan;
|
|
18179
|
+
}
|
|
17920
18180
|
function shouldForceReconnect(consecutiveHeartbeatFailures) {
|
|
17921
18181
|
if (consecutiveHeartbeatFailures < 2) return false;
|
|
17922
18182
|
return consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0;
|
|
@@ -19964,7 +20224,7 @@ async function startDaemon(options) {
|
|
|
19964
20224
|
try {
|
|
19965
20225
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
19966
20226
|
if (!dir) return;
|
|
19967
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
20227
|
+
const { reconcileServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
|
|
19968
20228
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
19969
20229
|
const config = readSvampConfig(configPath);
|
|
19970
20230
|
const entries = Array.from(urls.entries());
|
|
@@ -19986,7 +20246,7 @@ async function startDaemon(options) {
|
|
|
19986
20246
|
try {
|
|
19987
20247
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
19988
20248
|
if (!dir) return;
|
|
19989
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
20249
|
+
const { reconcileServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
|
|
19990
20250
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
19991
20251
|
const config = readSvampConfig(configPath);
|
|
19992
20252
|
const incoming = [{
|
|
@@ -20007,7 +20267,7 @@ async function startDaemon(options) {
|
|
|
20007
20267
|
try {
|
|
20008
20268
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
20009
20269
|
if (!dir) return;
|
|
20010
|
-
const { dropServiceLinks } = await import('./agentCommands-
|
|
20270
|
+
const { dropServiceLinks } = await import('./agentCommands-BiSVzb5D.mjs');
|
|
20011
20271
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
20012
20272
|
const config = readSvampConfig(configPath);
|
|
20013
20273
|
if (dropServiceLinks(config, "serve", mountName)) {
|
|
@@ -20050,7 +20310,7 @@ async function startDaemon(options) {
|
|
|
20050
20310
|
ensureAutoInstalledCommands(logger);
|
|
20051
20311
|
(async () => {
|
|
20052
20312
|
try {
|
|
20053
|
-
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-
|
|
20313
|
+
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-BO0Qs_kE.mjs');
|
|
20054
20314
|
beginClaudeVersionReconcile((msg) => logger.log(msg));
|
|
20055
20315
|
} catch (e) {
|
|
20056
20316
|
logger.log(`[claude-version] check failed: ${e?.message || e}`);
|
|
@@ -21270,15 +21530,19 @@ ${parts.join("\n")}`);
|
|
|
21270
21530
|
sessionService.updateAgentState({
|
|
21271
21531
|
controlledByUser: false,
|
|
21272
21532
|
requests: reqs,
|
|
21273
|
-
|
|
21274
|
-
|
|
21275
|
-
|
|
21533
|
+
// #1041: bounded + slimmed. `arguments: toolInput` used to
|
|
21534
|
+
// be stored here — the whole tool input, e.g. a full Write
|
|
21535
|
+
// body — in a map that grew forever and is re-walked by the
|
|
21536
|
+
// reducer on every streamed batch.
|
|
21537
|
+
completedRequests: recordCompletedRequest(
|
|
21538
|
+
sessionService._agentState?.completedRequests,
|
|
21539
|
+
correlationId,
|
|
21540
|
+
{
|
|
21276
21541
|
tool: toolName,
|
|
21277
|
-
arguments: toolInput,
|
|
21278
21542
|
completedAt: Date.now(),
|
|
21279
21543
|
status: result.behavior === "allow" ? "approved" : "denied"
|
|
21280
21544
|
}
|
|
21281
|
-
|
|
21545
|
+
)
|
|
21282
21546
|
});
|
|
21283
21547
|
}).catch((err) => {
|
|
21284
21548
|
logger.log(`[Session ${sessionId}] Permission handler error (request ${requestId}): ${err}`);
|
|
@@ -22314,11 +22578,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
22314
22578
|
} else {
|
|
22315
22579
|
logger.log(`[Session ${sessionId}] Queuing inbox message for idle processing`);
|
|
22316
22580
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
22317
|
-
|
|
22318
|
-
|
|
22319
|
-
messageQueue: [...existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId }]
|
|
22320
|
-
};
|
|
22581
|
+
const { queue: cappedQueue, dropped } = appendToMessageQueue(existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId });
|
|
22582
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: cappedQueue };
|
|
22321
22583
|
sessionService.updateMetadata(sessionMetadata);
|
|
22584
|
+
if (dropped > 0) {
|
|
22585
|
+
logger.log(`[Session ${sessionId}] Inbox queue over ${sessionQueueMax()}; dropped ${dropped} oldest queued inbox message(s)`);
|
|
22586
|
+
sessionService.pushMessage({ type: "message", message: `Inbox queue full (${sessionQueueMax()}) \u2014 dropped ${dropped} older queued message(s).`, level: "warning" }, "event");
|
|
22587
|
+
}
|
|
22322
22588
|
if (!sessionWasProcessing) {
|
|
22323
22589
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
22324
22590
|
}
|
|
@@ -22377,11 +22643,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
22377
22643
|
});
|
|
22378
22644
|
},
|
|
22379
22645
|
onIssue: async (params) => {
|
|
22380
|
-
const { issueRpc } = await import('./rpc-
|
|
22646
|
+
const { issueRpc } = await import('./rpc-DXY3KqiA.mjs');
|
|
22381
22647
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
22382
22648
|
},
|
|
22383
22649
|
onWorkflow: async (params) => {
|
|
22384
|
-
const { workflowRpc } = await import('./rpc-
|
|
22650
|
+
const { workflowRpc } = await import('./rpc-hdw0ZlCj.mjs');
|
|
22385
22651
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
22386
22652
|
},
|
|
22387
22653
|
onRipgrep: async (args, cwd) => {
|
|
@@ -22992,11 +23258,13 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
22992
23258
|
} else {
|
|
22993
23259
|
logger.log(`[${agentName} Session ${sessionId}] Queuing inbox message for idle processing`);
|
|
22994
23260
|
const existingQueue = sessionMetadata.messageQueue || [];
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
messageQueue: [...existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId }]
|
|
22998
|
-
};
|
|
23261
|
+
const { queue: cappedQueue, dropped } = appendToMessageQueue(existingQueue, { id: message.messageId, text: formatted, displayText: formatted, createdAt: Date.now(), inboxHop: message.hopCount ?? 0, inboxThreadId: message.threadId || message.messageId });
|
|
23262
|
+
sessionMetadata = { ...sessionMetadata, messageQueue: cappedQueue };
|
|
22999
23263
|
sessionService.updateMetadata(sessionMetadata);
|
|
23264
|
+
if (dropped > 0) {
|
|
23265
|
+
logger.log(`[${agentName} Session ${sessionId}] Inbox queue over ${sessionQueueMax()}; dropped ${dropped} oldest queued inbox message(s)`);
|
|
23266
|
+
sessionService.pushMessage({ type: "message", message: `Inbox queue full (${sessionQueueMax()}) \u2014 dropped ${dropped} older queued message(s).`, level: "warning" }, "event");
|
|
23267
|
+
}
|
|
23000
23268
|
if (sessionMetadata.lifecycleState === "idle" && acpBackendReady) {
|
|
23001
23269
|
const queue = sessionMetadata.messageQueue;
|
|
23002
23270
|
if (queue && queue.length > 0) {
|
|
@@ -23085,11 +23353,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
23085
23353
|
});
|
|
23086
23354
|
},
|
|
23087
23355
|
onIssue: async (params) => {
|
|
23088
|
-
const { issueRpc } = await import('./rpc-
|
|
23356
|
+
const { issueRpc } = await import('./rpc-DXY3KqiA.mjs');
|
|
23089
23357
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
23090
23358
|
},
|
|
23091
23359
|
onWorkflow: async (params) => {
|
|
23092
|
-
const { workflowRpc } = await import('./rpc-
|
|
23360
|
+
const { workflowRpc } = await import('./rpc-hdw0ZlCj.mjs');
|
|
23093
23361
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
23094
23362
|
},
|
|
23095
23363
|
onRipgrep: async (args, cwd) => {
|
|
@@ -23997,7 +24265,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
23997
24265
|
const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
|
|
23998
24266
|
if (channelHttpPort > 0) {
|
|
23999
24267
|
try {
|
|
24000
|
-
const { createChannelHttpServer } = await import('./httpServer-
|
|
24268
|
+
const { createChannelHttpServer } = await import('./httpServer-Be1Hcddr.mjs');
|
|
24001
24269
|
const channelHttpServer = createChannelHttpServer({
|
|
24002
24270
|
getSessionIds: () => {
|
|
24003
24271
|
const ids = [];
|
|
@@ -24113,11 +24381,27 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24113
24381
|
}
|
|
24114
24382
|
return null;
|
|
24115
24383
|
});
|
|
24384
|
+
serveManager$1.setCapabilityAuthorizer((sessionId, token) => {
|
|
24385
|
+
if (!token || !token.startsWith("ck_")) return false;
|
|
24386
|
+
let cwd = null;
|
|
24387
|
+
for (const [, session] of pidToTrackedSession) {
|
|
24388
|
+
if (session.svampSessionId === sessionId && !session.stopped && session.directory) {
|
|
24389
|
+
cwd = session.directory;
|
|
24390
|
+
break;
|
|
24391
|
+
}
|
|
24392
|
+
}
|
|
24393
|
+
if (!cwd) return false;
|
|
24394
|
+
try {
|
|
24395
|
+
return channelKeyGrantsFileAccess(new ChannelStore(cwd).list(), token);
|
|
24396
|
+
} catch {
|
|
24397
|
+
return false;
|
|
24398
|
+
}
|
|
24399
|
+
});
|
|
24116
24400
|
const sessionFilesToken = createHash("sha256").update(`session-files:${machineId}`).digest("hex").slice(0, 20);
|
|
24117
24401
|
serveManager$1.ensureSessionFilesGateway(sessionFilesToken).then((base) => logger.log(`[serve] session-files gateway: ${base || "(tunnel pending)"}`)).catch((err) => logger.error(`[serve] session-files gateway failed: ${err?.message || err}`));
|
|
24118
24402
|
const persistedSessions = loadPersistedSessions();
|
|
24403
|
+
const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
|
|
24119
24404
|
try {
|
|
24120
|
-
const knownIds = new Set(persistedSessions.map((s) => s.sessionId).concat(Object.keys(loadSessionIndex())));
|
|
24121
24405
|
const { removed } = await sweepOrphanedStagedHomes(knownIds);
|
|
24122
24406
|
if (removed.length > 0) {
|
|
24123
24407
|
logger.log(`Cleaned up ${removed.length} orphaned staged home(s) from prior runs.`);
|
|
@@ -24128,7 +24412,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24128
24412
|
const sessionsToAutoContinue = [];
|
|
24129
24413
|
const sessionsToLoopResume = [];
|
|
24130
24414
|
{
|
|
24131
|
-
const knownSessionIds =
|
|
24415
|
+
const knownSessionIds = knownIds;
|
|
24132
24416
|
const sweptDirs = /* @__PURE__ */ new Set();
|
|
24133
24417
|
for (const p of persistedSessions) {
|
|
24134
24418
|
if (sweptDirs.has(p.directory)) continue;
|
|
@@ -24153,7 +24437,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24153
24437
|
}
|
|
24154
24438
|
if (persistedSessions.length > 0) {
|
|
24155
24439
|
try {
|
|
24156
|
-
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-
|
|
24440
|
+
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-BO0Qs_kE.mjs');
|
|
24157
24441
|
await awaitClaudeVersionReady();
|
|
24158
24442
|
} catch {
|
|
24159
24443
|
}
|
|
@@ -24410,7 +24694,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24410
24694
|
const PING_TIMEOUT_MS = 15e3;
|
|
24411
24695
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
24412
24696
|
const RECONNECT_JITTER_MS = 2500;
|
|
24413
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
24697
|
+
const { WorkflowScheduler } = await import('./scheduler-3LUL1jFD.mjs');
|
|
24414
24698
|
const workflowProjectRoots = () => {
|
|
24415
24699
|
const dirs = /* @__PURE__ */ new Set();
|
|
24416
24700
|
for (const s of pidToTrackedSession.values()) {
|
|
@@ -24489,7 +24773,9 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24489
24773
|
const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
|
|
24490
24774
|
const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
|
|
24491
24775
|
const hasActiveSessions = pidToTrackedSession.size > 0;
|
|
24776
|
+
let zombieProbeRan = false;
|
|
24492
24777
|
if (shouldRunZombieProbe({ hasActiveSessions, inboundSilenceMs, consecutiveHeartbeatFailures, inGrace, thresholdMs: INBOUND_SILENCE_THRESHOLD_MS })) {
|
|
24778
|
+
zombieProbeRan = true;
|
|
24493
24779
|
logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
|
|
24494
24780
|
try {
|
|
24495
24781
|
const machineServiceId = `${server.config.client_id}:default`;
|
|
@@ -24502,7 +24788,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
24502
24788
|
consecutiveHeartbeatFailures = nextFailuresAfterZombieProbeFailure(consecutiveHeartbeatFailures);
|
|
24503
24789
|
}
|
|
24504
24790
|
}
|
|
24505
|
-
if (
|
|
24791
|
+
if (shouldRunServiceProbe({ inGrace, zombieProbeRan })) {
|
|
24506
24792
|
try {
|
|
24507
24793
|
const pingStart = Date.now();
|
|
24508
24794
|
const machineServiceId = `${server.config.client_id}:default`;
|