svamp-cli 0.2.346 → 0.2.358
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/skills/artifact/SKILL.md +19 -5
- package/bin/skills/crew/SKILL.md +42 -11
- package/dist/{acpBackend-BZ1MJhQV.mjs → acpBackend-UaBD4zL3.mjs} +3 -0
- package/dist/{agentCommands-B2Nl7Fof.mjs → agentCommands-C_KvvYFp.mjs} +5 -5
- package/dist/{claudeAuth-iAerdaMr.mjs → claudeAuth-DMDvPNhO.mjs} +55 -6
- package/dist/cli.mjs +164 -86
- package/dist/{commands-D69mnkYu.mjs → commands-B19lVZjm.mjs} +10 -3
- package/dist/{commands-BB1Kcyt-.mjs → commands-Bmoy6Rlw.mjs} +34 -4
- package/dist/{commands-CiRCwjkJ.mjs → commands-CFBKkfX8.mjs} +9 -9
- package/dist/{commands-D1pZq3d4.mjs → commands-CcdoQIkb.mjs} +126 -25
- package/dist/{commands-BOpoVBOM.mjs → commands-DH_YQrAP.mjs} +5 -21
- package/dist/{commands-H4PsTGn8.mjs → commands-DRxANaS5.mjs} +65 -19
- package/dist/{commands-C2aODeVc.mjs → commands-nkvTnUJj.mjs} +29 -13
- package/dist/{detectIsolation-CzoDCEnh.mjs → detectIsolation-CrV8IFDv.mjs} +36 -16
- package/dist/{fleet-DM_Q7w69.mjs → fleet-Ca-UjJNL.mjs} +40 -15
- package/dist/friendlyName-B-OEwt0t.mjs +341 -0
- package/dist/{headlessCli-C8HzQvis.mjs → headlessCli-BpTXz6FD.mjs} +24 -16
- package/dist/{hookSettings-DYMTUgVA.mjs → hookSettings-CvvUhwCK.mjs} +294 -55
- package/dist/{friendlyName-1UZcyDhu.mjs → inboxGuard-CDqmIypF.mjs} +32 -347
- package/dist/index.mjs +10 -9
- package/dist/{loopVerify-B47sSjxe.mjs → loopVerify-CxAKJB5a.mjs} +1 -1
- package/dist/{notifyCommands-D-IKIxiq.mjs → notifyCommands-CCUE18Q-.mjs} +1 -1
- package/dist/package-DTTWvJbp.mjs +64 -0
- package/dist/{rpc-Cf0grcru.mjs → rpc-CAlAHQAy.mjs} +8 -3
- package/dist/{rpc-BB6RF4-F.mjs → rpc-CacreLQ8.mjs} +13 -9
- package/dist/{run-ChdsgaSD.mjs → run-Bc81iurl.mjs} +27 -8
- package/dist/{run-DkjwF-zA.mjs → run-DKOND4dO.mjs} +222 -73
- package/dist/{scheduler-BY30ZrB9.mjs → scheduler-IyLWBF8T.mjs} +28 -10
- package/dist/{serveAuth-DbVLRCRR.mjs → serveAuth-4M0vwMyj.mjs} +53 -11
- package/dist/{serveCommands-CUHUiYnZ.mjs → serveCommands-B-Ym_QXc.mjs} +8 -8
- package/dist/{store-Bn0ByYev.mjs → store-D8dok6_d.mjs} +62 -14
- package/package.json +3 -3
- package/dist/package-DtuINqf5.mjs +0 -64
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import * as fs from 'fs';
|
|
2
|
-
import { mkdirSync, chmodSync, writeFileSync, unlinkSync, existsSync, readFileSync, renameSync as renameSync$1 } from 'fs';
|
|
2
|
+
import { statSync, mkdirSync, chmodSync, writeFileSync, unlinkSync, existsSync, readFileSync, renameSync as renameSync$1 } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import path__default, { join } from 'path';
|
|
5
5
|
import { randomUUID as randomUUID$1, randomBytes as randomBytes$1, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
@@ -17,15 +17,15 @@ import * as net from 'net';
|
|
|
17
17
|
import { createServer } from 'net';
|
|
18
18
|
import { homedir, platform, arch } from 'os';
|
|
19
19
|
import { d as getFrpsSubdomainHost, e as getFrpsServerPort, f as getFrpsServerAddr, h as getHyphaServerUrl } from './instance-CeHz5C8v.mjs';
|
|
20
|
-
import { S as ServeAuth, s as sanitizeLoginReturn, h as hasCookieToken, a as stripServeTokenParam, b as sanitizeForwardHeaders, c as stripServeAuthCookie } from './serveAuth-
|
|
21
|
-
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, rmSync, cpSync, realpathSync, readdirSync, statSync
|
|
20
|
+
import { S as ServeAuth, s as sanitizeLoginReturn, h as hasCookieToken, a as stripServeTokenParam, b as sanitizeForwardHeaders, c as stripServeAuthCookie } from './serveAuth-4M0vwMyj.mjs';
|
|
21
|
+
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, chmodSync as chmodSync$1, renameSync, rmSync, cpSync, realpathSync, readdirSync, statSync as statSync$1, appendFileSync, openSync, readSync, closeSync, fstatSync, writeSync, unlinkSync as unlinkSync$1 } from 'node:fs';
|
|
22
22
|
import os, { homedir as homedir$1 } from 'node:os';
|
|
23
23
|
import { join as join$1, extname, resolve, sep, basename, dirname } from 'node:path';
|
|
24
24
|
import { e as envFlagEnabled, a as envFlagDisabled } from './envFlag-COOuJ3AH.mjs';
|
|
25
|
-
import { normalizeProxyUrl, resolveHyphaProxyUrl, applyClaudeProxyEnv } from './claudeAuth-
|
|
25
|
+
import { normalizeProxyUrl, resolveHyphaProxyUrl, applyClaudeProxyEnv } from './claudeAuth-DMDvPNhO.mjs';
|
|
26
26
|
import { CODEX_PROVIDER_KEY_ENV } from './codexProvider-BtxYc5Rj.mjs';
|
|
27
27
|
import { spawn as spawn$1 } from 'node:child_process';
|
|
28
|
-
import {
|
|
28
|
+
import { applyInboxClear } from './inboxGuard-CDqmIypF.mjs';
|
|
29
29
|
import { w as withFileLock, s as sleepSync } from './fileLock-BMPwCrB6.mjs';
|
|
30
30
|
import { EventEmitter } from 'node:events';
|
|
31
31
|
import { r as renderTemplate } from './cron-hHGdb5kU.mjs';
|
|
@@ -74,6 +74,19 @@ function levelToRole(level) {
|
|
|
74
74
|
const entry = Object.entries(ROLE_HIERARCHY).find(([, v]) => v === level);
|
|
75
75
|
return entry?.[0] ?? null;
|
|
76
76
|
}
|
|
77
|
+
function isOwnerEquivalentStrict(context, sharing) {
|
|
78
|
+
if (!context?.user) return true;
|
|
79
|
+
if (isSameOwnerWorkspace(context)) return true;
|
|
80
|
+
if (!sharing) return false;
|
|
81
|
+
const userEmail = context.user.email;
|
|
82
|
+
if (userEmail && sharing.owner && userEmail.toLowerCase() === sharing.owner.toLowerCase()) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function isAdminEquivalentStrict(context, sharing) {
|
|
86
|
+
if (!context?.user) return true;
|
|
87
|
+
if (isSameOwnerWorkspace(context)) return true;
|
|
88
|
+
return !!sharing && getEffectiveRole(context, sharing) === "admin";
|
|
89
|
+
}
|
|
77
90
|
function authorizeRequest(context, sharing, requiredRole = "view") {
|
|
78
91
|
if (!context?.user) return;
|
|
79
92
|
if (isSameOwnerWorkspace(context)) return;
|
|
@@ -124,6 +137,18 @@ function workflowOpMinRole(op) {
|
|
|
124
137
|
if (typeof op === "string" && WORKFLOW_READ_OPS.has(op)) return "view";
|
|
125
138
|
return "admin";
|
|
126
139
|
}
|
|
140
|
+
function issueVerifyRefusalReason(params, isOwner) {
|
|
141
|
+
if (isOwner) return null;
|
|
142
|
+
const p = params;
|
|
143
|
+
if (!p || typeof p !== "object") return null;
|
|
144
|
+
const op = typeof p.op === "string" ? p.op : "";
|
|
145
|
+
if (op !== "add" && op !== "edit" && op !== "triage") return null;
|
|
146
|
+
const v = p.verify;
|
|
147
|
+
const structuredIsCommand = !!v && typeof v === "object" && v.type === "command";
|
|
148
|
+
const flatCommand = typeof p.verifyCmd === "string" && p.verifyCmd.length > 0;
|
|
149
|
+
if (!structuredIsCommand && !flatCommand) return null;
|
|
150
|
+
return "Access denied: only the session owner may attach a command verify-cmd. It is executed by the daemon outside the sandbox when the issue is closed. Use --verify '<criterion>' (an agent-evaluated check) or --manual instead.";
|
|
151
|
+
}
|
|
127
152
|
|
|
128
153
|
function toSchema(t) {
|
|
129
154
|
return { name: t.name, description: t.description, parameters: t.parameters || { type: "object", properties: {} } };
|
|
@@ -279,6 +304,17 @@ const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
|
|
|
279
304
|
const MAX_RESTART_DELAY_S = 300;
|
|
280
305
|
const BACKOFF_RESET_WINDOW_MS = 6e4;
|
|
281
306
|
const MAX_LOG_LINES = 300;
|
|
307
|
+
function checkWorkdir(workdir, stat = statSync) {
|
|
308
|
+
if (!workdir) return null;
|
|
309
|
+
try {
|
|
310
|
+
if (!stat(workdir).isDirectory()) return `workdir is not a directory: ${workdir}`;
|
|
311
|
+
return null;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err?.code === "ENOENT") return `workdir does not exist: ${workdir}`;
|
|
314
|
+
if (err?.code === "EACCES") return `workdir is not accessible: ${workdir}`;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
282
318
|
const CRASH_LOOP_FAILURE_THRESHOLD = 10;
|
|
283
319
|
function accumulateCrashLoopFailure(prev, healthy, now) {
|
|
284
320
|
if (healthy) return { consecutiveFailures: 0, failureWindowStart: void 0 };
|
|
@@ -567,8 +603,11 @@ class ProcessSupervisor {
|
|
|
567
603
|
if (!file.endsWith(".json")) continue;
|
|
568
604
|
try {
|
|
569
605
|
const raw = await readFile(path__default.join(this.persistDir, file), "utf-8");
|
|
570
|
-
const
|
|
571
|
-
const
|
|
606
|
+
const parsed = JSON.parse(raw);
|
|
607
|
+
const restored = parsed?.__state;
|
|
608
|
+
delete parsed.__state;
|
|
609
|
+
const spec = parsed;
|
|
610
|
+
const entry = this.makeEntry(spec, restored);
|
|
572
611
|
this.entries.set(spec.id, entry);
|
|
573
612
|
if (spec.keepAlive && !spec.desiredStopped) {
|
|
574
613
|
await this.startEntry(
|
|
@@ -591,12 +630,49 @@ class ProcessSupervisor {
|
|
|
591
630
|
console.log(`[SUPERVISOR] Restored ${loaded} supervised process(es)`);
|
|
592
631
|
}
|
|
593
632
|
}
|
|
594
|
-
|
|
633
|
+
/**
|
|
634
|
+
* #1643: the crash-loop LEDGER travels with the spec.
|
|
635
|
+
*
|
|
636
|
+
* `ProcessState` is annotated "not persisted", and `makeEntry` zeroes restartCount /
|
|
637
|
+
* consecutiveFailures / crashLoopBackOff on every restore — so a permanently-broken keepAlive
|
|
638
|
+
* spec (dead upstream, taken port, vanished workdir) got a FRESH 10-restart budget on every
|
|
639
|
+
* daemon start, forever. It burns CRASH_LOOP_FAILURE_THRESHOLD restarts and ~15 min of
|
|
640
|
+
* exponential spawn churn, parks crash-loop-backoff, and starts over on the next boot —
|
|
641
|
+
* undermining the cap that exists because of the "11,508 restarts in 4 days" incident, and
|
|
642
|
+
* worst right after a reboot when several dead specs churn at once. `svamp process list` also
|
|
643
|
+
* showed RESTARTS 0 after every restart, which hid it. The #1633 docblock states this for the
|
|
644
|
+
* missing-workdir case; it is general.
|
|
645
|
+
*
|
|
646
|
+
* Written under a `__state` key so the file stays a superset of ProcessSpec: an older daemon
|
|
647
|
+
* ignores it, and `loadAll` strips it before the object becomes `entry.spec`.
|
|
648
|
+
*/
|
|
649
|
+
async persistSpec(spec, state, failureWindowStart) {
|
|
595
650
|
const filePath = path__default.join(this.persistDir, `${spec.id}.json`);
|
|
651
|
+
if (!state) {
|
|
652
|
+
const live = this.entries.get(spec.id);
|
|
653
|
+
if (live) {
|
|
654
|
+
state = live.state;
|
|
655
|
+
failureWindowStart = live.failureWindowStart;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
596
658
|
const tmpPath = `${filePath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
597
|
-
|
|
659
|
+
const payload = { ...spec };
|
|
660
|
+
if (state) {
|
|
661
|
+
payload.__state = {
|
|
662
|
+
restartCount: state.restartCount,
|
|
663
|
+
userRestartCount: state.userRestartCount,
|
|
664
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
665
|
+
crashLoopBackOff: state.crashLoopBackOff,
|
|
666
|
+
failureWindowStart
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
598
670
|
await rename(tmpPath, filePath);
|
|
599
671
|
}
|
|
672
|
+
/** #1643: rewrite this spec's file including its live crash-loop ledger. Best-effort. */
|
|
673
|
+
persistLedger(entry) {
|
|
674
|
+
void this.persistSpec(entry.spec, entry.state, entry.failureWindowStart).catch((err) => console.error(`[SUPERVISOR] Failed to persist crash-loop ledger for ${entry.spec.name}: ${err?.message || err}`));
|
|
675
|
+
}
|
|
600
676
|
async deleteSpec(id) {
|
|
601
677
|
try {
|
|
602
678
|
await unlink(path__default.join(this.persistDir, `${id}.json`));
|
|
@@ -604,17 +680,22 @@ class ProcessSupervisor {
|
|
|
604
680
|
}
|
|
605
681
|
}
|
|
606
682
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
607
|
-
makeEntry(spec) {
|
|
683
|
+
makeEntry(spec, restored) {
|
|
608
684
|
return {
|
|
609
685
|
spec,
|
|
610
686
|
state: {
|
|
611
687
|
id: spec.id,
|
|
612
688
|
status: "pending",
|
|
613
|
-
|
|
614
|
-
|
|
689
|
+
// #1643: a restored ledger carries the crash-loop budget ACROSS the restart. Without
|
|
690
|
+
// it, `maxRestarts` and CRASH_LOOP_FAILURE_THRESHOLD were per-boot rather than
|
|
691
|
+
// lifetime caps, which is not what either of them claims to be.
|
|
692
|
+
restartCount: restored?.restartCount ?? 0,
|
|
693
|
+
userRestartCount: restored?.userRestartCount ?? 0,
|
|
615
694
|
consecutiveProbeFailures: 0,
|
|
616
|
-
consecutiveFailures: 0
|
|
695
|
+
consecutiveFailures: restored?.consecutiveFailures ?? 0,
|
|
696
|
+
crashLoopBackOff: restored?.crashLoopBackOff
|
|
617
697
|
},
|
|
698
|
+
failureWindowStart: restored?.failureWindowStart,
|
|
618
699
|
logBuffer: [],
|
|
619
700
|
stopping: false
|
|
620
701
|
};
|
|
@@ -654,6 +735,16 @@ class ProcessSupervisor {
|
|
|
654
735
|
const { spec, state } = entry;
|
|
655
736
|
if (entry.deleted) return;
|
|
656
737
|
state.consecutiveProbeFailures = 0;
|
|
738
|
+
const workdirProblem = checkWorkdir(spec.workdir);
|
|
739
|
+
if (workdirProblem) {
|
|
740
|
+
state.status = "failed";
|
|
741
|
+
state.stoppedAt = Date.now();
|
|
742
|
+
entry.logBuffer.push(`[supervisor] ${workdirProblem}`);
|
|
743
|
+
console.error(
|
|
744
|
+
`[SUPERVISOR] Not starting '${spec.name}': ${workdirProblem} (permanent \u2014 not retrying; fix the path or run 'svamp process restart ${spec.name}')`
|
|
745
|
+
);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
657
748
|
try {
|
|
658
749
|
const env = { ...process.env, ...spec.env ?? {} };
|
|
659
750
|
const child = spawn(spec.command, spec.args, {
|
|
@@ -735,6 +826,7 @@ class ProcessSupervisor {
|
|
|
735
826
|
);
|
|
736
827
|
return;
|
|
737
828
|
}
|
|
829
|
+
this.persistLedger(entry);
|
|
738
830
|
if (spec.maxRestarts > 0 && state.restartCount >= spec.maxRestarts) {
|
|
739
831
|
console.warn(`[SUPERVISOR] Process '${spec.name}' reached max restarts (${spec.maxRestarts}), not restarting`);
|
|
740
832
|
state.status = "failed";
|
|
@@ -758,6 +850,7 @@ class ProcessSupervisor {
|
|
|
758
850
|
if (entry.child) return;
|
|
759
851
|
state.restartCount++;
|
|
760
852
|
state.status = "starting";
|
|
853
|
+
this.persistLedger(entry);
|
|
761
854
|
this.spawnProcess(entry);
|
|
762
855
|
}, delayMs);
|
|
763
856
|
}
|
|
@@ -2068,9 +2161,10 @@ function serveStaticMount(req, res, opts) {
|
|
|
2068
2161
|
return;
|
|
2069
2162
|
}
|
|
2070
2163
|
if (st.isDirectory()) {
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2164
|
+
const normalizedPath = (relPath || "/").split("?")[0];
|
|
2165
|
+
const redirectTo = ("/" + normalizedPath.replace(/^\/+/, "")).replace(/\/+$/, "") + "/";
|
|
2166
|
+
if (!normalizedPath.endsWith("/")) {
|
|
2167
|
+
res.writeHead(302, { ...CORS, Location: mountUrlPrefix ? mountUrlPrefix.replace(/\/$/, "") + redirectTo : redirectTo });
|
|
2074
2168
|
res.end();
|
|
2075
2169
|
return;
|
|
2076
2170
|
}
|
|
@@ -2251,7 +2345,13 @@ function resolveMountOwnerEmail(suppliedOwnerEmail, callerEmail, machineOwnerEma
|
|
|
2251
2345
|
function sanitizeMountForRole(mount, isAdmin) {
|
|
2252
2346
|
if (isAdmin) return mount;
|
|
2253
2347
|
const { linkToken: _lt, directory: _dir, process: proc, url, ...rest } = mount;
|
|
2254
|
-
const safeProcess = proc ?
|
|
2348
|
+
const safeProcess = proc ? (() => {
|
|
2349
|
+
const { env, workdir: _wd, command: _cmd, args: _args, ...procRest } = proc;
|
|
2350
|
+
return {
|
|
2351
|
+
...procRest,
|
|
2352
|
+
env: env ? Object.fromEntries(Object.keys(env).map((k) => [k, "***"])) : env
|
|
2353
|
+
};
|
|
2354
|
+
})() : proc;
|
|
2255
2355
|
const safeUrl = mount.access === "link" ? void 0 : url;
|
|
2256
2356
|
return { ...rest, process: safeProcess, ...safeUrl !== void 0 ? { url: safeUrl } : {} };
|
|
2257
2357
|
}
|
|
@@ -3410,6 +3510,13 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3410
3510
|
res.end("Write access denied: public mounts are read-only.");
|
|
3411
3511
|
return;
|
|
3412
3512
|
}
|
|
3513
|
+
const writeBadSeg = hiddenSegmentOf(basePath);
|
|
3514
|
+
if (writeBadSeg) {
|
|
3515
|
+
this.log(`Write DENY '${mountName}': blocked dotfile segment '${writeBadSeg}' (${basePath})`);
|
|
3516
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
3517
|
+
res.end("Forbidden: hidden/dot-prefixed paths are not writable.");
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3413
3520
|
const filePath = resolveContained();
|
|
3414
3521
|
if (!filePath) {
|
|
3415
3522
|
this.log(`Write DENY '${mountName}': path escapes mount root (${basePath})`);
|
|
@@ -3419,13 +3526,17 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3419
3526
|
}
|
|
3420
3527
|
if (req.method === "PUT") {
|
|
3421
3528
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3422
|
-
const
|
|
3529
|
+
const tmpPath = path.join(
|
|
3530
|
+
path.dirname(filePath),
|
|
3531
|
+
`.svamp-put-${process.pid}-${crypto.randomBytes(6).toString("hex")}.part`
|
|
3532
|
+
);
|
|
3533
|
+
const ws = fs.createWriteStream(tmpPath);
|
|
3423
3534
|
let done = false;
|
|
3424
3535
|
const cleanupPartial = () => {
|
|
3425
3536
|
if (done) return;
|
|
3426
3537
|
done = true;
|
|
3427
3538
|
ws.destroy();
|
|
3428
|
-
fs.unlink(
|
|
3539
|
+
fs.unlink(tmpPath, () => {
|
|
3429
3540
|
});
|
|
3430
3541
|
};
|
|
3431
3542
|
req.on("aborted", cleanupPartial);
|
|
@@ -3434,13 +3545,25 @@ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:und
|
|
|
3434
3545
|
});
|
|
3435
3546
|
req.pipe(ws);
|
|
3436
3547
|
ws.on("finish", () => {
|
|
3548
|
+
if (done) return;
|
|
3437
3549
|
done = true;
|
|
3550
|
+
try {
|
|
3551
|
+
fs.renameSync(tmpPath, filePath);
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
fs.unlink(tmpPath, () => {
|
|
3554
|
+
});
|
|
3555
|
+
res.writeHead(500);
|
|
3556
|
+
res.end(err?.message || "rename failed");
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3438
3559
|
res.writeHead(201);
|
|
3439
3560
|
res.end();
|
|
3440
3561
|
});
|
|
3441
3562
|
ws.on("error", (err) => {
|
|
3442
3563
|
if (done) return;
|
|
3443
3564
|
done = true;
|
|
3565
|
+
fs.unlink(tmpPath, () => {
|
|
3566
|
+
});
|
|
3444
3567
|
res.writeHead(500);
|
|
3445
3568
|
res.end(err.message);
|
|
3446
3569
|
});
|
|
@@ -3823,9 +3946,13 @@ function loadAccounts(svampHome) {
|
|
|
3823
3946
|
if (!existsSync$1(file)) return [];
|
|
3824
3947
|
try {
|
|
3825
3948
|
const parsed = JSON.parse(readFileSync$1(file, "utf-8"));
|
|
3826
|
-
if (!parsed || !Array.isArray(parsed.accounts))
|
|
3949
|
+
if (!parsed || !Array.isArray(parsed.accounts)) {
|
|
3950
|
+
console.warn(`[accounts] ${file} is present but malformed \u2014 treating as empty. Configured backend accounts will appear to have vanished; restore from a backup rather than re-adding blindly. (#1629)`);
|
|
3951
|
+
return [];
|
|
3952
|
+
}
|
|
3827
3953
|
return parsed.accounts;
|
|
3828
|
-
} catch {
|
|
3954
|
+
} catch (err) {
|
|
3955
|
+
console.warn(`[accounts] failed to read ${file}: ${err?.message || err}. Treating as empty \u2014 every configured backend account will appear to have vanished. (#1629)`);
|
|
3829
3956
|
return [];
|
|
3830
3957
|
}
|
|
3831
3958
|
}
|
|
@@ -3834,7 +3961,13 @@ function saveAccounts(accounts, svampHome) {
|
|
|
3834
3961
|
const dir = join$1(file, "..");
|
|
3835
3962
|
if (!existsSync$1(dir)) mkdirSync$1(dir, { recursive: true });
|
|
3836
3963
|
const payload = { version: 1, accounts };
|
|
3837
|
-
|
|
3964
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
3965
|
+
writeFileSync$1(tmp, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
3966
|
+
try {
|
|
3967
|
+
chmodSync$1(tmp, 384);
|
|
3968
|
+
} catch {
|
|
3969
|
+
}
|
|
3970
|
+
renameSync(tmp, file);
|
|
3838
3971
|
try {
|
|
3839
3972
|
chmodSync$1(file, 384);
|
|
3840
3973
|
} catch {
|
|
@@ -4520,7 +4653,7 @@ function validateUpload(input) {
|
|
|
4520
4653
|
if (existsSync$1(destDir)) {
|
|
4521
4654
|
const count = readdirSync(destDir).filter((f) => {
|
|
4522
4655
|
try {
|
|
4523
|
-
return statSync(join$1(destDir, f)).isFile();
|
|
4656
|
+
return statSync$1(join$1(destDir, f)).isFile();
|
|
4524
4657
|
} catch {
|
|
4525
4658
|
return false;
|
|
4526
4659
|
}
|
|
@@ -4733,7 +4866,7 @@ class ChannelStore {
|
|
|
4733
4866
|
list() {
|
|
4734
4867
|
let st;
|
|
4735
4868
|
try {
|
|
4736
|
-
const s = statSync(this.dir);
|
|
4869
|
+
const s = statSync$1(this.dir);
|
|
4737
4870
|
st = { mtimeMs: s.mtimeMs, size: s.size };
|
|
4738
4871
|
} catch {
|
|
4739
4872
|
_channelListCache.delete(this.dir);
|
|
@@ -5202,7 +5335,7 @@ class ChannelOutbox {
|
|
|
5202
5335
|
/** Current on-disk (mtimeMs, size), or (0, -1) if the file does not exist. */
|
|
5203
5336
|
_stat() {
|
|
5204
5337
|
try {
|
|
5205
|
-
const st = statSync(this.file);
|
|
5338
|
+
const st = statSync$1(this.file);
|
|
5206
5339
|
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
5207
5340
|
} catch {
|
|
5208
5341
|
return { mtimeMs: 0, size: -1 };
|
|
@@ -5442,6 +5575,9 @@ const SESSION_METHOD_MIN_ROLE = {
|
|
|
5442
5575
|
channelReply: "admin",
|
|
5443
5576
|
updateAgentState: "admin",
|
|
5444
5577
|
switchMode: "admin",
|
|
5578
|
+
// #1628: rotating the outpost token DESTROYS a live capability and mints a new one — admin,
|
|
5579
|
+
// like outpostConnectInfo which hands the same capability out.
|
|
5580
|
+
outpostRotateToken: "admin",
|
|
5445
5581
|
restartClaude: "admin",
|
|
5446
5582
|
archiveSession: "admin",
|
|
5447
5583
|
readFile: "admin",
|
|
@@ -5977,6 +6113,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
5977
6113
|
const callerEmail = context?.user?.email;
|
|
5978
6114
|
const machineOwner = currentMetadata.sharing?.owner;
|
|
5979
6115
|
const isSharedUser = callerEmail && machineOwner && callerEmail.toLowerCase() !== machineOwner.toLowerCase();
|
|
6116
|
+
if (options.sharing?.allowedUsers?.length) {
|
|
6117
|
+
options = {
|
|
6118
|
+
...options,
|
|
6119
|
+
sharing: {
|
|
6120
|
+
...options.sharing,
|
|
6121
|
+
allowedUsers: options.sharing.allowedUsers.map((u) => normalizeAllowedUser(u, "spawn"))
|
|
6122
|
+
}
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
5980
6125
|
if (isSharedUser) {
|
|
5981
6126
|
const sharing = {
|
|
5982
6127
|
enabled: true,
|
|
@@ -6014,7 +6159,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6014
6159
|
} else if (options.sharing?.enabled && !options.sharing.owner && callerEmail) {
|
|
6015
6160
|
options = {
|
|
6016
6161
|
...options,
|
|
6017
|
-
sharing: {
|
|
6162
|
+
sharing: {
|
|
6163
|
+
...options.sharing,
|
|
6164
|
+
owner: callerEmail,
|
|
6165
|
+
...options.sharing.allowedUsers ? { allowedUsers: options.sharing.allowedUsers.map((u) => normalizeAllowedUser(u, "spawn-explicit")) } : {}
|
|
6166
|
+
}
|
|
6018
6167
|
};
|
|
6019
6168
|
}
|
|
6020
6169
|
if (currentMetadata.securityContextConfig && callerEmail) {
|
|
@@ -6356,12 +6505,12 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6356
6505
|
return { success: true };
|
|
6357
6506
|
},
|
|
6358
6507
|
// Shell access
|
|
6359
|
-
bash: async (command, cwd, context) => {
|
|
6508
|
+
bash: async (command, cwd, timeoutMs, context) => {
|
|
6360
6509
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
6361
6510
|
const { exec } = await import('child_process');
|
|
6362
6511
|
const { homedir } = await import('os');
|
|
6363
6512
|
return new Promise((resolve) => {
|
|
6364
|
-
exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
|
|
6513
|
+
exec(command, { cwd: cwd || homedir(), timeout: timeoutMs ?? 3e4, maxBuffer: EXEC_MAX_BUFFER }, (err, stdout, stderr) => {
|
|
6365
6514
|
if (err) resolve(execFailureOutcome(err, stdout, stderr));
|
|
6366
6515
|
else resolve(execSuccessOutcome(stdout, stderr));
|
|
6367
6516
|
});
|
|
@@ -7477,7 +7626,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
7477
7626
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
7478
7627
|
}
|
|
7479
7628
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
7480
|
-
const { queryCore } = await import('./commands-
|
|
7629
|
+
const { queryCore } = await import('./commands-CcdoQIkb.mjs').then(function (n) { return n.d; });
|
|
7481
7630
|
const timeout = c.reply?.timeout_sec || 120;
|
|
7482
7631
|
let result;
|
|
7483
7632
|
let thrownSessionId;
|
|
@@ -7746,7 +7895,7 @@ ${d?.error || "not found"}`;
|
|
|
7746
7895
|
const CHUNK_BYTES$1 = 1024 * 1024;
|
|
7747
7896
|
const LF = 10;
|
|
7748
7897
|
function countFileLines(path) {
|
|
7749
|
-
const size = statSync(path).size;
|
|
7898
|
+
const size = statSync$1(path).size;
|
|
7750
7899
|
if (size === 0) return 0;
|
|
7751
7900
|
const fd = openSync(path, "r");
|
|
7752
7901
|
try {
|
|
@@ -7864,6 +8013,14 @@ function loadInbox(projectDir, sessionId) {
|
|
|
7864
8013
|
return [];
|
|
7865
8014
|
}
|
|
7866
8015
|
}
|
|
8016
|
+
const reportedInboxWriteErrnos = /* @__PURE__ */ new Set();
|
|
8017
|
+
function reportInboxWriteFailure(projectDir, sessionId, err) {
|
|
8018
|
+
const code = String(err?.code || err?.name || "UNKNOWN");
|
|
8019
|
+
if (reportedInboxWriteErrnos.has(code)) return;
|
|
8020
|
+
reportedInboxWriteErrnos.add(code);
|
|
8021
|
+
const hint = code === "ENOSPC" ? ' \u2014 THE DISK IS FULL. Pending inter-agent messages will be LOST on the next daemon restart and `inbox reply` will answer "not found"; session transcripts are likely failing too.' : code === "EROFS" ? " \u2014 the filesystem is read-only." : code === "EACCES" || code === "EPERM" ? " \u2014 permission denied on the session .svamp directory." : code === "ENOTDIR" ? " \u2014 a path component of .svamp/<sessionId>/ is a FILE, not a directory." : "";
|
|
8022
|
+
console.error(`[inbox] persist failed for session ${sessionId} in ${projectDir} (${code})${hint} Further ${code} failures will not be repeated. (#1714)`);
|
|
8023
|
+
}
|
|
7867
8024
|
function saveInbox(projectDir, sessionId, inbox) {
|
|
7868
8025
|
try {
|
|
7869
8026
|
const p = inboxFilePath(projectDir, sessionId);
|
|
@@ -7871,7 +8028,8 @@ function saveInbox(projectDir, sessionId, inbox) {
|
|
|
7871
8028
|
const tmp = `${p}.tmp-${process.pid}-${randomBytes$1(4).toString("hex")}`;
|
|
7872
8029
|
writeFileSync$1(tmp, JSON.stringify(inbox));
|
|
7873
8030
|
renameSync(tmp, p);
|
|
7874
|
-
} catch {
|
|
8031
|
+
} catch (err) {
|
|
8032
|
+
reportInboxWriteFailure(projectDir, sessionId, err);
|
|
7875
8033
|
}
|
|
7876
8034
|
}
|
|
7877
8035
|
function isPending(m) {
|
|
@@ -7984,6 +8142,17 @@ function ensureOutpostToken(projectDir, session) {
|
|
|
7984
8142
|
}
|
|
7985
8143
|
return rec;
|
|
7986
8144
|
}
|
|
8145
|
+
function rotateOutpostToken(projectDir, session) {
|
|
8146
|
+
const rec = { version: 1, session, token: genToken(), createdAt: Date.now() };
|
|
8147
|
+
const file = tokenPath(projectDir, session);
|
|
8148
|
+
mkdirSync$1(dirname(file), { recursive: true });
|
|
8149
|
+
writeFileSync$1(file, JSON.stringify(rec, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
8150
|
+
try {
|
|
8151
|
+
chmodSync$1(file, 384);
|
|
8152
|
+
} catch {
|
|
8153
|
+
}
|
|
8154
|
+
return rec;
|
|
8155
|
+
}
|
|
7987
8156
|
function outpostTokenMatches(rec, presented) {
|
|
7988
8157
|
if (!rec || typeof presented !== "string" || presented.length !== rec.token.length) return false;
|
|
7989
8158
|
let diff = 0;
|
|
@@ -8151,25 +8320,34 @@ async function _mintPairingCodeLocked(opts, resolver, ttlMs) {
|
|
|
8151
8320
|
stage: true,
|
|
8152
8321
|
_rkwargs: true
|
|
8153
8322
|
});
|
|
8154
|
-
const putUrl = await am.put_file({ artifact_id: artifactId, file_path: "pair.json", download_weight: 0, _rkwargs: true });
|
|
8155
|
-
if (!putUrl || typeof putUrl !== "string") throw new Error(`put_file returned invalid URL: ${putUrl}`);
|
|
8156
|
-
const controller = new AbortController();
|
|
8157
|
-
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
8158
|
-
try {
|
|
8159
|
-
const resp = await fetch(putUrl, {
|
|
8160
|
-
method: "PUT",
|
|
8161
|
-
body: pairJson,
|
|
8162
|
-
headers: { "Content-Type": "application/json" },
|
|
8163
|
-
signal: controller.signal
|
|
8164
|
-
});
|
|
8165
|
-
if (!resp.ok) throw new Error(`pair.json upload failed: ${resp.status} ${resp.statusText}`);
|
|
8166
|
-
} finally {
|
|
8167
|
-
clearTimeout(timer);
|
|
8168
|
-
}
|
|
8169
|
-
await am.commit({ artifact_id: artifactId, _rkwargs: true });
|
|
8170
8323
|
const all = loadPairings(opts.svampHome);
|
|
8171
8324
|
all.push({ session, code, artifactId, expiresAt, createdAt: now });
|
|
8172
8325
|
savePairings(all, opts.svampHome);
|
|
8326
|
+
try {
|
|
8327
|
+
const putUrl = await am.put_file({ artifact_id: artifactId, file_path: "pair.json", download_weight: 0, _rkwargs: true });
|
|
8328
|
+
if (!putUrl || typeof putUrl !== "string") throw new Error(`put_file returned invalid URL: ${putUrl}`);
|
|
8329
|
+
const controller = new AbortController();
|
|
8330
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
8331
|
+
try {
|
|
8332
|
+
const resp = await fetch(putUrl, {
|
|
8333
|
+
method: "PUT",
|
|
8334
|
+
body: pairJson,
|
|
8335
|
+
headers: { "Content-Type": "application/json" },
|
|
8336
|
+
signal: controller.signal
|
|
8337
|
+
});
|
|
8338
|
+
if (!resp.ok) throw new Error(`pair.json upload failed: ${resp.status} ${resp.statusText}`);
|
|
8339
|
+
} finally {
|
|
8340
|
+
clearTimeout(timer);
|
|
8341
|
+
}
|
|
8342
|
+
await am.commit({ artifact_id: artifactId, _rkwargs: true });
|
|
8343
|
+
} catch (err) {
|
|
8344
|
+
try {
|
|
8345
|
+
await deletePairingArtifact(am, artifactId, log);
|
|
8346
|
+
savePairings(loadPairings(opts.svampHome).filter((p) => p.artifactId !== artifactId), opts.svampHome);
|
|
8347
|
+
} catch {
|
|
8348
|
+
}
|
|
8349
|
+
throw err;
|
|
8350
|
+
}
|
|
8173
8351
|
log?.(`[OUTPOST PAIRING] minted ${code} for ${session} (expires in ${Math.round(ttlMs / 6e4)}m)`);
|
|
8174
8352
|
return { code, resolver, expiresAt };
|
|
8175
8353
|
}
|
|
@@ -8395,6 +8573,24 @@ function channelPublicView(c) {
|
|
|
8395
8573
|
function isStructuredMessage(msg) {
|
|
8396
8574
|
return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
|
|
8397
8575
|
}
|
|
8576
|
+
function reportInboxEviction(result, sessionId) {
|
|
8577
|
+
for (const m of result.droppedUndelivered) {
|
|
8578
|
+
console.warn(
|
|
8579
|
+
`[HYPHA SESSION ${sessionId}] inbox at cap: DROPPED AN UNDELIVERED message ${m.messageId || "<no id>"} from ${m.from || "<unknown>"} (urgency=${m.urgency || "normal"}). Every resident message was undelivered, so there was no delivered one to evict instead. The sender was told it was sent; it is now gone (#0328).`
|
|
8580
|
+
);
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
function evictInboxOverflow(inbox, max) {
|
|
8584
|
+
const dropped = [];
|
|
8585
|
+
const droppedUndelivered = [];
|
|
8586
|
+
while (inbox.length > max) {
|
|
8587
|
+
const idx = inbox.findIndex((m) => m.handled);
|
|
8588
|
+
const victim = inbox.splice(idx < 0 ? 0 : idx, 1)[0];
|
|
8589
|
+
dropped.push(victim);
|
|
8590
|
+
if (!victim.handled) droppedUndelivered.push(victim);
|
|
8591
|
+
}
|
|
8592
|
+
return { dropped, droppedUndelivered };
|
|
8593
|
+
}
|
|
8398
8594
|
function escapeXml(s) {
|
|
8399
8595
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8400
8596
|
}
|
|
@@ -8430,10 +8626,10 @@ function formatInboxMessageXml(msg) {
|
|
|
8430
8626
|
}
|
|
8431
8627
|
if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
|
|
8432
8628
|
if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
|
|
8433
|
-
if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
|
|
8629
|
+
if (msg.urgency) attrs.push(`urgency="${escapeXml(String(msg.urgency))}"`);
|
|
8434
8630
|
if (msg.replyTo) attrs.push(`reply-to="${escapeXml(msg.replyTo)}"`);
|
|
8435
8631
|
if (msg.threadId) attrs.push(`thread-id="${escapeXml(msg.threadId)}"`);
|
|
8436
|
-
attrs.push(`timestamp="${msg.timestamp}"`);
|
|
8632
|
+
attrs.push(`timestamp="${escapeXml(String(msg.timestamp))}"`);
|
|
8437
8633
|
return `<svamp-message ${attrs.join(" ")}>
|
|
8438
8634
|
${msg.body}
|
|
8439
8635
|
</svamp-message>`;
|
|
@@ -8544,7 +8740,7 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
|
8544
8740
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
8545
8741
|
if (!existsSync$1(filePath)) return fallbackInMemory;
|
|
8546
8742
|
try {
|
|
8547
|
-
const st = statSync(filePath);
|
|
8743
|
+
const st = statSync$1(filePath);
|
|
8548
8744
|
const cached = messageCountCache.get(filePath);
|
|
8549
8745
|
if (cached && cached.size === st.size && cached.mtimeMs === st.mtimeMs) return cached.count;
|
|
8550
8746
|
let count;
|
|
@@ -8803,7 +8999,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8803
8999
|
} else if (role === "session") {
|
|
8804
9000
|
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
8805
9001
|
} else {
|
|
8806
|
-
const
|
|
9002
|
+
const rawText = typeof content === "string" ? content : content?.text ?? content?.content ?? content;
|
|
9003
|
+
const text = typeof rawText === "string" ? rawText : rawText && typeof rawText === "object" && typeof rawText.text === "string" ? rawText.text : JSON.stringify(content);
|
|
8807
9004
|
wrappedContent = { role: "user", content: { type: "text", text } };
|
|
8808
9005
|
}
|
|
8809
9006
|
const msg = {
|
|
@@ -8853,7 +9050,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8853
9050
|
subject: `Remote outpost connected: ${who}`
|
|
8854
9051
|
};
|
|
8855
9052
|
inbox.push(msg);
|
|
8856
|
-
|
|
9053
|
+
reportInboxEviction(evictInboxOverflow(inbox, INBOX_MAX), sessionId);
|
|
8857
9054
|
syncInboxToMetadata();
|
|
8858
9055
|
callbacks.onInboxMessage?.(msg);
|
|
8859
9056
|
notifyListeners({ type: "inbox-update", sessionId, message: msg });
|
|
@@ -9099,7 +9296,21 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9099
9296
|
}
|
|
9100
9297
|
}
|
|
9101
9298
|
const isBtwEcho = !!(parsed && parsed.meta && parsed.meta.btw) || !!(meta && meta.btw);
|
|
9102
|
-
const
|
|
9299
|
+
const coerceUserText = (t, depth = 0) => {
|
|
9300
|
+
if (typeof t === "string") return t;
|
|
9301
|
+
if (t && typeof t === "object" && depth < 4) {
|
|
9302
|
+
if (typeof t.text === "string") return t.text;
|
|
9303
|
+
if (t.text && typeof t.text === "object") return coerceUserText(t.text, depth + 1);
|
|
9304
|
+
}
|
|
9305
|
+
return typeof t === "undefined" ? "" : JSON.stringify(t);
|
|
9306
|
+
};
|
|
9307
|
+
const coerceUserContent = (c) => {
|
|
9308
|
+
if (typeof c === "string") return { type: "text", text: c };
|
|
9309
|
+
if (c && typeof c === "object" && c.type === "text") {
|
|
9310
|
+
return { ...c, text: coerceUserText(c.text) };
|
|
9311
|
+
}
|
|
9312
|
+
return c;
|
|
9313
|
+
};
|
|
9103
9314
|
const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: coerceUserContent(parsed.content) } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
|
|
9104
9315
|
if (isBtwEcho) wrappedContent.meta = { btw: true };
|
|
9105
9316
|
const msg = {
|
|
@@ -9276,6 +9487,32 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9276
9487
|
// `svamp outpost up`, so the install command never drifts. NO channel is created — the remote
|
|
9277
9488
|
// dials the machine's public `:outpost` gateway and this session's token authenticates it.
|
|
9278
9489
|
// Admin-gated: the returned token is a capability to run commands on whatever machine connects.
|
|
9490
|
+
// #1628: REVOKE this session's outpost token by minting a fresh one, then return the new
|
|
9491
|
+
// install info. The durable token is a capability ("whoever holds it can run commands on
|
|
9492
|
+
// connected outposts") that never expired and had no rotate path — and the documented
|
|
9493
|
+
// workflow PRINTS it in an install one-liner meant to be pasted into someone else's
|
|
9494
|
+
// terminal, so it routinely lands in shell history, chat and screen shares. Every prior
|
|
9495
|
+
// outpost fix (#0757, #0778, #1035, #1374) closed a LEAK; none gave the operator a way to
|
|
9496
|
+
// invalidate a token that had already leaked.
|
|
9497
|
+
//
|
|
9498
|
+
// Admin-gated like outpostConnectInfo — this both destroys a capability and mints a new one.
|
|
9499
|
+
// Live pairing codes for the session are burned in the same act: leaving a code that
|
|
9500
|
+
// resolves to the OLD token would defeat the rotation.
|
|
9501
|
+
outpostRotateToken: async (params, context) => {
|
|
9502
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
9503
|
+
if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
|
|
9504
|
+
try {
|
|
9505
|
+
rotateOutpostToken(outpostProjectDir(), sessionId);
|
|
9506
|
+
try {
|
|
9507
|
+
const am = await server.getService("public/artifact-manager");
|
|
9508
|
+
if (am) await burnSessionPairings(am, sessionId, void 0, (m) => console.log(m));
|
|
9509
|
+
} catch {
|
|
9510
|
+
}
|
|
9511
|
+
} catch (err) {
|
|
9512
|
+
return { error: `could not rotate the outpost token: ${err?.message || err}` };
|
|
9513
|
+
}
|
|
9514
|
+
return await rpcHandlers.outpostConnectInfo(params, context);
|
|
9515
|
+
},
|
|
9279
9516
|
outpostConnectInfo: async (params, context) => {
|
|
9280
9517
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
9281
9518
|
if (!outpostServiceId) return { error: "the outpost service is not available on this machine" };
|
|
@@ -9694,7 +9931,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9694
9931
|
authorizeRequest(context, metadata.sharing, "interact");
|
|
9695
9932
|
let forwarded = params;
|
|
9696
9933
|
if (params && (params.mode !== void 0 || params.allowTools !== void 0)) {
|
|
9697
|
-
if (!
|
|
9934
|
+
if (!isAdminEquivalentStrict(context, metadata.sharing)) {
|
|
9698
9935
|
const { mode: _droppedMode, allowTools: _droppedTools, ...rest } = params;
|
|
9699
9936
|
forwarded = rest;
|
|
9700
9937
|
console.warn(
|
|
@@ -9812,6 +10049,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9812
10049
|
// workflow writes 'admin' (direct server-side command exec). See issueOpMinRole above.
|
|
9813
10050
|
issue: async (params, context) => {
|
|
9814
10051
|
authorizeRequest(context, metadata.sharing, issueOpMinRole(params?.op));
|
|
10052
|
+
const refusal = issueVerifyRefusalReason(params, isOwnerEquivalentStrict(context, metadata.sharing));
|
|
10053
|
+
if (refusal) throw new Error(refusal);
|
|
9815
10054
|
if (!callbacks.onIssue) throw new Error("issue not supported");
|
|
9816
10055
|
return await callbacks.onIssue(params);
|
|
9817
10056
|
},
|
|
@@ -9928,7 +10167,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
9928
10167
|
const trusted = !context?.user || isSameOwnerWorkspace(context) || !!metadata.sharing && getEffectiveRole(context, metadata.sharing) === "admin";
|
|
9929
10168
|
const msg = sanitizeInboundInboxMessage(message, { trusted, callerEmail: context?.user?.email });
|
|
9930
10169
|
inbox.push(msg);
|
|
9931
|
-
|
|
10170
|
+
reportInboxEviction(evictInboxOverflow(inbox, INBOX_MAX), sessionId);
|
|
9932
10171
|
syncInboxToMetadata();
|
|
9933
10172
|
callbacks.onInboxMessage?.(msg);
|
|
9934
10173
|
notifyListeners({ type: "inbox-update", sessionId, message: msg });
|