@mcpjam/inspector 3.3.6 → 3.4.1
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 +33 -0
- package/bin/launch-workspace.d.mts +17 -0
- package/bin/launch-workspace.mjs +74 -0
- package/bin/start.js +31 -0
- package/dist/client/assets/dead-clicks-autocapture-D8PonUmY.js +2 -0
- package/dist/client/assets/exception-autocapture-D8fwMBrA.js +2 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-DSAkJIuh.js +2 -0
- package/dist/client/assets/index-Cd1YrfPX.css +1 -0
- package/dist/client/assets/{index-CPz3J6s3.js → index-DRqGwDPP.js} +8 -2488
- package/dist/client/assets/jszip.min-CBm444cl.js +2 -0
- package/dist/client/assets/posthog-recorder-Cc0E-JHG.js +2 -0
- package/dist/client/assets/surveys-DAzKgVVH.js +2 -0
- package/dist/client/assets/web-vitals-BTZc1cda.js +2 -0
- package/dist/client/claude-desktop-logo.png +0 -0
- package/dist/client/guest-credit-wall.png +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/user-testing-empty.png +0 -0
- package/dist/server/{chunk-7TOT3ZWN.js → chunk-5TXZXCUR.js} +1089 -133
- package/dist/server/harness-install-cli.js +1 -1
- package/dist/server/index.js +56359 -26517
- package/package.json +10 -4
- package/dist/client/assets/dead-clicks-autocapture-BMgZJ0Wv.js +0 -2
- package/dist/client/assets/exception-autocapture-HqVjr75I.js +0 -3
- package/dist/client/assets/highlighted-body-OFNGDK62-CZgdeyBU.js +0 -2
- package/dist/client/assets/index-BArhVpAv.css +0 -1
- package/dist/client/assets/jszip.min-DzIFtwMm.js +0 -3
- package/dist/client/assets/posthog-recorder-BeJOU0JC.js +0 -109
- package/dist/client/assets/surveys-BZ6tGdti.js +0 -2
- package/dist/client/assets/web-vitals--9YBaAs8.js +0 -2
|
@@ -12,11 +12,19 @@ var ALLOWED_ENVIRONMENTS = [
|
|
|
12
12
|
"local",
|
|
13
13
|
"test"
|
|
14
14
|
];
|
|
15
|
+
var ENVIRONMENT_ALIASES = {
|
|
16
|
+
production: "prod",
|
|
17
|
+
development: "dev"
|
|
18
|
+
};
|
|
15
19
|
function resolveEnvironment() {
|
|
16
|
-
const fromEnv = process.env.ENVIRONMENT;
|
|
20
|
+
const fromEnv = process.env.ENVIRONMENT?.trim();
|
|
17
21
|
if (fromEnv && ALLOWED_ENVIRONMENTS.includes(fromEnv)) {
|
|
18
22
|
return fromEnv;
|
|
19
23
|
}
|
|
24
|
+
const aliased = fromEnv ? ENVIRONMENT_ALIASES[fromEnv] : void 0;
|
|
25
|
+
if (aliased) {
|
|
26
|
+
return aliased;
|
|
27
|
+
}
|
|
20
28
|
if (process.env.NODE_ENV === "test") return "test";
|
|
21
29
|
if (process.env.NODE_ENV === "production") {
|
|
22
30
|
if (!warnedMissingEnv) {
|
|
@@ -29,7 +37,7 @@ function resolveEnvironment() {
|
|
|
29
37
|
}
|
|
30
38
|
return "dev";
|
|
31
39
|
}
|
|
32
|
-
var BAKED_VERSION = "3.
|
|
40
|
+
var BAKED_VERSION = "3.4.1";
|
|
33
41
|
function blankToNull(value) {
|
|
34
42
|
return value === void 0 || value.trim() === "" ? null : value;
|
|
35
43
|
}
|
|
@@ -342,19 +350,19 @@ function emit(eventName, base, payload, options) {
|
|
|
342
350
|
process.on("beforeExit", () => logger.flush());
|
|
343
351
|
|
|
344
352
|
// server/utils/harness/local/runtime-install.ts
|
|
345
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
353
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
346
354
|
import { createReadStream, createWriteStream } from "fs";
|
|
347
355
|
import {
|
|
348
|
-
mkdir,
|
|
349
|
-
readdir as
|
|
350
|
-
readFile as
|
|
351
|
-
rename,
|
|
352
|
-
rm,
|
|
353
|
-
stat as
|
|
354
|
-
writeFile
|
|
356
|
+
mkdir as mkdir3,
|
|
357
|
+
readdir as readdir4,
|
|
358
|
+
readFile as readFile5,
|
|
359
|
+
rename as rename2,
|
|
360
|
+
rm as rm3,
|
|
361
|
+
stat as stat4,
|
|
362
|
+
writeFile as writeFile3
|
|
355
363
|
} from "fs/promises";
|
|
356
364
|
import { homedir } from "os";
|
|
357
|
-
import { basename, join as
|
|
365
|
+
import { basename, join as join5 } from "path";
|
|
358
366
|
import { pipeline } from "stream/promises";
|
|
359
367
|
import { Readable } from "stream";
|
|
360
368
|
import { x as extractTar } from "tar";
|
|
@@ -424,7 +432,7 @@ function verifyPackManifestSignature(manifestBytes, signatureBase64, keys = PACK
|
|
|
424
432
|
import { createHash } from "crypto";
|
|
425
433
|
import { execFile as execFile2 } from "child_process";
|
|
426
434
|
import { readdir as readdir2, readFile as readFile2, realpath, stat } from "fs/promises";
|
|
427
|
-
import { dirname, isAbsolute, join, normalize, relative, sep } from "path";
|
|
435
|
+
import { dirname, isAbsolute, join as join2, normalize, relative, sep } from "path";
|
|
428
436
|
|
|
429
437
|
// server/utils/harness/local/targets.ts
|
|
430
438
|
var LOCAL_PACK_TARGETS = /* @__PURE__ */ new Set([
|
|
@@ -448,8 +456,9 @@ function currentLocalPlatform(platform = process.platform) {
|
|
|
448
456
|
}
|
|
449
457
|
|
|
450
458
|
// server/utils/harness/local/process-identity.ts
|
|
451
|
-
import { execFile } from "child_process";
|
|
459
|
+
import { execFile, spawn } from "child_process";
|
|
452
460
|
import { readdir, readFile } from "fs/promises";
|
|
461
|
+
import { join } from "path";
|
|
453
462
|
var PS_TIMEOUT_MS = 3e3;
|
|
454
463
|
function parseLinuxProcStat(raw) {
|
|
455
464
|
const close = raw.lastIndexOf(")");
|
|
@@ -534,12 +543,125 @@ async function probeDarwin(pid) {
|
|
|
534
543
|
identity: `darwin:${parsed.lstart}|${parsed.command}`
|
|
535
544
|
};
|
|
536
545
|
}
|
|
546
|
+
var WIN32_PROBE_TIMEOUT_MS = 15e3;
|
|
547
|
+
function win32SystemRoot() {
|
|
548
|
+
return process.env.SYSTEMROOT ?? process.env.WINDIR ?? "C:\\Windows";
|
|
549
|
+
}
|
|
550
|
+
function win32PowerShellPath() {
|
|
551
|
+
return join(
|
|
552
|
+
win32SystemRoot(),
|
|
553
|
+
"System32",
|
|
554
|
+
"WindowsPowerShell",
|
|
555
|
+
"v1.0",
|
|
556
|
+
"powershell.exe"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
function parseWin32ProbeLine(raw) {
|
|
560
|
+
const line = raw.trim();
|
|
561
|
+
const match = /^(\d+)\|(.+)$/.exec(line);
|
|
562
|
+
if (match === null) return null;
|
|
563
|
+
return { fileTime: match[1], name: match[2] };
|
|
564
|
+
}
|
|
565
|
+
async function probeWin32(pid) {
|
|
566
|
+
try {
|
|
567
|
+
process.kill(pid, 0);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const code = error.code;
|
|
570
|
+
if (code === "ESRCH") return { state: "gone" };
|
|
571
|
+
if (code !== "EPERM") {
|
|
572
|
+
return {
|
|
573
|
+
state: "unknown",
|
|
574
|
+
reason: `kill(0) failed (${code ?? "unknown"})`
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const script = [
|
|
579
|
+
`$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue`,
|
|
580
|
+
"if ($null -eq $p) { exit 1 }",
|
|
581
|
+
"try { $t = $p.StartTime.ToFileTimeUtc() } catch { exit 3 }",
|
|
582
|
+
"Write-Output ('{0}|{1}' -f $t, $p.ProcessName)"
|
|
583
|
+
].join("; ");
|
|
584
|
+
const result = await new Promise((resolve) => {
|
|
585
|
+
let settled = false;
|
|
586
|
+
let timer;
|
|
587
|
+
const done = (value) => {
|
|
588
|
+
if (settled) return;
|
|
589
|
+
settled = true;
|
|
590
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
591
|
+
resolve(value);
|
|
592
|
+
};
|
|
593
|
+
let child;
|
|
594
|
+
try {
|
|
595
|
+
child = spawn(
|
|
596
|
+
win32PowerShellPath(),
|
|
597
|
+
[
|
|
598
|
+
"-NoProfile",
|
|
599
|
+
"-NonInteractive",
|
|
600
|
+
"-InputFormat",
|
|
601
|
+
"None",
|
|
602
|
+
"-ExecutionPolicy",
|
|
603
|
+
"Bypass",
|
|
604
|
+
"-Command",
|
|
605
|
+
script
|
|
606
|
+
],
|
|
607
|
+
{ stdio: ["ignore", "pipe", "pipe"], env: process.env, windowsHide: true }
|
|
608
|
+
);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
done({
|
|
611
|
+
ok: false,
|
|
612
|
+
exitCode: null,
|
|
613
|
+
reason: `PowerShell failed to start (${String(
|
|
614
|
+
error.code ?? "unknown"
|
|
615
|
+
)})`
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
let stdout = "";
|
|
620
|
+
child.stdout?.setEncoding("utf8");
|
|
621
|
+
child.stdout?.on("data", (chunk) => {
|
|
622
|
+
if (stdout.length < 16 * 1024) stdout += chunk;
|
|
623
|
+
});
|
|
624
|
+
child.stderr?.resume();
|
|
625
|
+
timer = setTimeout(() => {
|
|
626
|
+
try {
|
|
627
|
+
child.kill();
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
done({ ok: false, exitCode: null, reason: "Get-Process timed out" });
|
|
631
|
+
}, WIN32_PROBE_TIMEOUT_MS);
|
|
632
|
+
child.on(
|
|
633
|
+
"error",
|
|
634
|
+
(error) => done({
|
|
635
|
+
ok: false,
|
|
636
|
+
exitCode: null,
|
|
637
|
+
reason: `PowerShell failed (${String(error.code ?? "unknown")})`
|
|
638
|
+
})
|
|
639
|
+
);
|
|
640
|
+
child.on("close", (code) => {
|
|
641
|
+
if (code === 0) done({ ok: true, stdout });
|
|
642
|
+
else done({ ok: false, exitCode: code, reason: `Get-Process exited ${code}` });
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
if (!result.ok) {
|
|
646
|
+
if (result.exitCode === 1) return { state: "gone" };
|
|
647
|
+
return { state: "unknown", reason: result.reason };
|
|
648
|
+
}
|
|
649
|
+
const parsed = parseWin32ProbeLine(result.stdout);
|
|
650
|
+
if (parsed === null) {
|
|
651
|
+
return { state: "unknown", reason: "unparseable Get-Process output" };
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
state: "alive",
|
|
655
|
+
identity: `win32:${parsed.fileTime}|${parsed.name}`
|
|
656
|
+
};
|
|
657
|
+
}
|
|
537
658
|
async function probeProcess(pid, platform = process.platform) {
|
|
538
659
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
539
660
|
return { state: "unknown", reason: "implausible pid" };
|
|
540
661
|
}
|
|
541
662
|
if (platform === "linux") return probeLinux(pid);
|
|
542
663
|
if (platform === "darwin") return probeDarwin(pid);
|
|
664
|
+
if (platform === "win32") return probeWin32(pid);
|
|
543
665
|
return { state: "unknown", reason: `no liveness primitive on ${platform}` };
|
|
544
666
|
}
|
|
545
667
|
async function readProcessBirthIdentity(pid, platform = process.platform) {
|
|
@@ -640,6 +762,12 @@ async function probeProcessGroup(pid, platform = process.platform) {
|
|
|
640
762
|
if (!Number.isInteger(pid) || pid <= 0) return "unknown";
|
|
641
763
|
if (platform === "linux") return probeLinuxGroup(pid);
|
|
642
764
|
if (platform === "darwin") return probeDarwinGroup(pid);
|
|
765
|
+
if (platform === "win32") {
|
|
766
|
+
const root = await probeProcess(pid, platform);
|
|
767
|
+
if (root.state === "gone") return "empty";
|
|
768
|
+
if (root.state === "alive") return "live";
|
|
769
|
+
return "unknown";
|
|
770
|
+
}
|
|
643
771
|
return "unknown";
|
|
644
772
|
}
|
|
645
773
|
async function readProcessGroupId(pid, platform = process.platform) {
|
|
@@ -743,7 +871,8 @@ function signalProcessGroup(pid, signal, platform = process.platform) {
|
|
|
743
871
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
744
872
|
try {
|
|
745
873
|
if (platform === "win32") {
|
|
746
|
-
|
|
874
|
+
process.kill(pid, signal);
|
|
875
|
+
return true;
|
|
747
876
|
}
|
|
748
877
|
process.kill(-pid, signal);
|
|
749
878
|
return true;
|
|
@@ -841,7 +970,7 @@ async function digestTreeWithSnapshot(root) {
|
|
|
841
970
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
842
971
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
843
972
|
for (const entry of entries) {
|
|
844
|
-
const full =
|
|
973
|
+
const full = join2(dir, entry.name);
|
|
845
974
|
const rel = relative(root, full).split(sep).join("/");
|
|
846
975
|
if (entry.isSymbolicLink()) {
|
|
847
976
|
throw new Error(
|
|
@@ -979,7 +1108,7 @@ async function resolveManagedBundle(args) {
|
|
|
979
1108
|
};
|
|
980
1109
|
}
|
|
981
1110
|
const policy = manifest.runtime;
|
|
982
|
-
const root =
|
|
1111
|
+
const root = join2(runtimeRoot, policy.bundleName);
|
|
983
1112
|
let canonicalRoot;
|
|
984
1113
|
try {
|
|
985
1114
|
canonicalRoot = await realpath(root);
|
|
@@ -1023,7 +1152,7 @@ async function resolveManagedBundle(args) {
|
|
|
1023
1152
|
}
|
|
1024
1153
|
const digest = verification.digest;
|
|
1025
1154
|
const insideBundle = async (relativePath, label, requireExecutable) => {
|
|
1026
|
-
const full =
|
|
1155
|
+
const full = join2(canonicalRoot, relativePath);
|
|
1027
1156
|
if (!normalize(full).startsWith(canonicalRoot + sep)) {
|
|
1028
1157
|
return {
|
|
1029
1158
|
ok: false,
|
|
@@ -1196,7 +1325,7 @@ async function resolveSystemInstall(opts) {
|
|
|
1196
1325
|
const rejections = [];
|
|
1197
1326
|
for (const dir of searchPaths) {
|
|
1198
1327
|
for (const name of policy.executableNames) {
|
|
1199
|
-
const candidate =
|
|
1328
|
+
const candidate = join2(dir, name);
|
|
1200
1329
|
let canonical;
|
|
1201
1330
|
try {
|
|
1202
1331
|
canonical = await realpath(candidate);
|
|
@@ -1336,7 +1465,7 @@ async function detectSnapshotDrift(snapshot) {
|
|
|
1336
1465
|
const walk = async (dir) => {
|
|
1337
1466
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1338
1467
|
for (const entry of entries) {
|
|
1339
|
-
const full =
|
|
1468
|
+
const full = join2(dir, entry.name);
|
|
1340
1469
|
const rel = relative(snapshot.root, full).split(sep).join("/");
|
|
1341
1470
|
if (entry.isSymbolicLink()) return `a symlink appeared at ${rel}`;
|
|
1342
1471
|
if (entry.isDirectory()) {
|
|
@@ -1417,6 +1546,568 @@ async function revalidateRuntime(runtime) {
|
|
|
1417
1546
|
}
|
|
1418
1547
|
}
|
|
1419
1548
|
|
|
1549
|
+
// server/utils/harness/local/compatibility.ts
|
|
1550
|
+
var LOCAL_HARNESS_MANIFEST = {
|
|
1551
|
+
"claude-code": {
|
|
1552
|
+
harnessId: "claude-code",
|
|
1553
|
+
adapterVersion: "1.0.100",
|
|
1554
|
+
runtime: {
|
|
1555
|
+
source: "managed-bundle",
|
|
1556
|
+
bundleName: "claude-code",
|
|
1557
|
+
// Generated by the release pack build, and EMPTY until one has run.
|
|
1558
|
+
// Either way the result is `bundle-absent` before any tree is read, but
|
|
1559
|
+
// WHICH message depends on what is on disk: with no bundle directory —
|
|
1560
|
+
// today's state — `resolveManagedBundle` reports the missing bundle
|
|
1561
|
+
// first; with a directory present, the empty map has no entry for this
|
|
1562
|
+
// machine's `<os>-<arch>` and it names the target instead ("no pack has
|
|
1563
|
+
// been built for linux-x64"). Both fail closed; only the second names a
|
|
1564
|
+
// pack a user could go looking for.
|
|
1565
|
+
bundleDigest: PACK_TREE_DIGESTS["claude-code"],
|
|
1566
|
+
launcherRelativePath: "launcher.mjs",
|
|
1567
|
+
nodeLauncherRelativePath: "bin/node",
|
|
1568
|
+
jobLauncherRelativePath: "bin/mcpjam-job-launcher.exe",
|
|
1569
|
+
vendorPackages: {
|
|
1570
|
+
"@anthropic-ai/claude-code": "pinned-by-adapter-bridge-lockfile"
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
argvPolicy: { requiredFlags: [], deniedFlags: [] },
|
|
1574
|
+
configStrategy: "synthetic-home",
|
|
1575
|
+
supportsBuiltinToolApprovals: true,
|
|
1576
|
+
permissionProfileMapping: {
|
|
1577
|
+
"read-only": "allow-reads",
|
|
1578
|
+
"workspace-edits": "allow-edits"
|
|
1579
|
+
// `unrestricted` is deliberately absent: Claude Code CAN surface
|
|
1580
|
+
// approvals, so there is no reason to offer a profile that switches them
|
|
1581
|
+
// off on a host.
|
|
1582
|
+
},
|
|
1583
|
+
// Native eligibility is per platform. macOS and Linux have POSIX process
|
|
1584
|
+
// groups, which is what whole-tree cleanup is built on there.
|
|
1585
|
+
//
|
|
1586
|
+
// Windows has no process group; its whole-tree guarantee is a Job Object
|
|
1587
|
+
// with KILL_ON_JOB_CLOSE, created by `tools/mcpjam-job-launcher`, which
|
|
1588
|
+
// ships INSIDE the Windows pack so its bytes are covered by the tree
|
|
1589
|
+
// digest. Listing `win32` here is necessary but not sufficient:
|
|
1590
|
+
// `supportsOwnershipProof('win32')` still answers false until runtime
|
|
1591
|
+
// resolution has verified that launcher, so a pack without one keeps the
|
|
1592
|
+
// platform refused exactly as before.
|
|
1593
|
+
//
|
|
1594
|
+
// Earned, not asserted: the conformance suite's `windows-latest` leg runs
|
|
1595
|
+
// the full scenario and the abort scenario against the real pack and the
|
|
1596
|
+
// real vendor CLI, and its survivor scan is what turns "we wrote a Job
|
|
1597
|
+
// Object" into evidence that stopping a session stops everything it
|
|
1598
|
+
// started. That leg is gating; the day it goes red, this entry is wrong.
|
|
1599
|
+
nativePlatforms: ["darwin", "linux", "win32"],
|
|
1600
|
+
// Empty until a backend's escape probes actually pass (I6).
|
|
1601
|
+
isolatedBackends: {},
|
|
1602
|
+
lifecycleConformanceVersion: "",
|
|
1603
|
+
adapterBootstrapDir: ".harness-bootstrap/claude-code",
|
|
1604
|
+
adapterBootstrapFiles: [
|
|
1605
|
+
"package.json",
|
|
1606
|
+
"pnpm-lock.yaml",
|
|
1607
|
+
"pnpm-workspace.yaml",
|
|
1608
|
+
"bridge.mjs"
|
|
1609
|
+
],
|
|
1610
|
+
bridgeBundleDigest: `sha256:${"0".repeat(64)}`
|
|
1611
|
+
},
|
|
1612
|
+
codex: {
|
|
1613
|
+
harnessId: "codex",
|
|
1614
|
+
adapterVersion: "1.0.98",
|
|
1615
|
+
runtime: {
|
|
1616
|
+
source: "managed-bundle",
|
|
1617
|
+
bundleName: "codex",
|
|
1618
|
+
bundleDigest: PACK_TREE_DIGESTS.codex,
|
|
1619
|
+
launcherRelativePath: "launcher.mjs",
|
|
1620
|
+
nodeLauncherRelativePath: "bin/node",
|
|
1621
|
+
vendorPackages: {
|
|
1622
|
+
"@openai/codex-sdk": "pinned-by-adapter-bridge-lockfile"
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
argvPolicy: { requiredFlags: [], deniedFlags: [] },
|
|
1626
|
+
configStrategy: "explicit-config-root",
|
|
1627
|
+
supportsBuiltinToolApprovals: false,
|
|
1628
|
+
// No mapping at all: the adapter rejects every mode but `allow-all`, and
|
|
1629
|
+
// `allow-all` on a host with no outer boundary is the thing this plan
|
|
1630
|
+
// exists to prevent. Codex earns a mapping when its adapter gains a
|
|
1631
|
+
// reviewed restricted mode, or inside a verified isolation backend.
|
|
1632
|
+
permissionProfileMapping: {},
|
|
1633
|
+
nativePlatforms: [],
|
|
1634
|
+
isolatedBackends: {},
|
|
1635
|
+
lifecycleConformanceVersion: "",
|
|
1636
|
+
adapterBootstrapDir: ".harness-bootstrap/codex",
|
|
1637
|
+
adapterBootstrapFiles: ["package.json", "pnpm-lock.yaml", "bridge.mjs"],
|
|
1638
|
+
bridgeBundleDigest: `sha256:${"0".repeat(64)}`
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
function localPermissionModeFor(harnessId, permissionProfile, targetKind) {
|
|
1642
|
+
const manifests = LOCAL_HARNESS_MANIFEST;
|
|
1643
|
+
const manifest = Object.prototype.hasOwnProperty.call(manifests, harnessId) ? manifests[harnessId] : void 0;
|
|
1644
|
+
if (manifest === void 0) return null;
|
|
1645
|
+
if (permissionProfile === "unrestricted" && targetKind === "local-native") {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
return manifest.permissionProfileMapping[permissionProfile] ?? null;
|
|
1649
|
+
}
|
|
1650
|
+
function resolveLocalCompatibility(query, manifests = LOCAL_HARNESS_MANIFEST) {
|
|
1651
|
+
const manifest = Object.prototype.hasOwnProperty.call(
|
|
1652
|
+
manifests,
|
|
1653
|
+
query.harnessId
|
|
1654
|
+
) ? manifests[query.harnessId] : void 0;
|
|
1655
|
+
if (!manifest) {
|
|
1656
|
+
return {
|
|
1657
|
+
ok: false,
|
|
1658
|
+
status: "harness-not-supported",
|
|
1659
|
+
message: `${query.harnessId} has no reviewed local compatibility manifest. Run it hosted, or add a manifest entry with conformance evidence.`
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
if (query.installedAdapterVersion !== manifest.adapterVersion) {
|
|
1663
|
+
return {
|
|
1664
|
+
ok: false,
|
|
1665
|
+
status: "adapter-version-mismatch",
|
|
1666
|
+
message: `${query.harnessId} adapter ${query.installedAdapterVersion ?? "(version not supplied)"} is installed but the local manifest was reviewed against ${manifest.adapterVersion}. An adapter upgrade can change the command shapes the local provider translates, so it must be re-reviewed before local execution is enabled.`
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
if (manifest.lifecycleConformanceVersion === "") {
|
|
1670
|
+
return {
|
|
1671
|
+
ok: false,
|
|
1672
|
+
status: "conformance-missing",
|
|
1673
|
+
message: `${query.harnessId} has no recorded lifecycle conformance evidence. Local execution stays disabled until the conformance suite has been run for this harness/runtime/platform/mode tuple and its version is recorded in the manifest.`
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
if (query.platform === null) {
|
|
1677
|
+
return {
|
|
1678
|
+
ok: false,
|
|
1679
|
+
status: "platform-not-supported",
|
|
1680
|
+
// NOT "run the harness hosted instead". A requested-local turn has Send
|
|
1681
|
+
// disabled in this state (`localHarnessBlocksSend`), so nothing falls
|
|
1682
|
+
// back — and on a local Inspector with no cloud data plane there is no
|
|
1683
|
+
// hosted target to switch to either, which makes the instruction
|
|
1684
|
+
// unactionable exactly where it is most likely to be read.
|
|
1685
|
+
message: `local execution is not supported on this platform.`
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
if (query.targetKind === "local-native") {
|
|
1689
|
+
if (!manifest.nativePlatforms.includes(query.platform)) {
|
|
1690
|
+
const reason = manifest.nativePlatforms.length === 0 ? `${query.harnessId} is not eligible for native mode on any platform: its adapter cannot surface tool approvals, so native execution would give it the OS user's full authority with no approval gate. Use hosted, or a verified isolation backend.` : `${query.harnessId} native mode is not supported on ${query.platform} (supported: ${manifest.nativePlatforms.join(", ")}).`;
|
|
1691
|
+
return {
|
|
1692
|
+
ok: false,
|
|
1693
|
+
status: "native-not-eligible",
|
|
1694
|
+
message: reason
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
if (query.backend === void 0) {
|
|
1699
|
+
return {
|
|
1700
|
+
ok: false,
|
|
1701
|
+
status: "backend-not-verified",
|
|
1702
|
+
message: "an isolated target must name its isolation backend."
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const verifiedHere = manifest.isolatedBackends[query.platform] ?? [];
|
|
1706
|
+
if (!verifiedHere.includes(query.backend)) {
|
|
1707
|
+
return {
|
|
1708
|
+
ok: false,
|
|
1709
|
+
status: "backend-not-verified",
|
|
1710
|
+
message: `isolation backend ${query.backend} has not passed conformance for ${query.harnessId} on ${query.platform}. Isolated mode never falls back to native \u2014 run hosted until the backend is verified for this platform.`
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const permissionMode = manifest.permissionProfileMapping[query.permissionProfile];
|
|
1715
|
+
if (permissionMode === void 0) {
|
|
1716
|
+
const offered = Object.keys(manifest.permissionProfileMapping);
|
|
1717
|
+
return {
|
|
1718
|
+
ok: false,
|
|
1719
|
+
status: "permission-profile-not-supported",
|
|
1720
|
+
message: `${query.harnessId} does not offer the ${query.permissionProfile} permission profile locally` + (offered.length ? ` (offered: ${offered.join(", ")}).` : `; it has no locally offered profile at all.`)
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
if (query.permissionProfile === "unrestricted" && query.targetKind === "local-native") {
|
|
1724
|
+
return {
|
|
1725
|
+
ok: false,
|
|
1726
|
+
status: "permission-profile-not-supported",
|
|
1727
|
+
message: `the unrestricted profile requires a verified isolation backend. Native mode has no host containment, so an unrestricted turn there would run with the OS user's full authority.`
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
return { ok: true, manifest, permissionMode };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// server/utils/harness/local/runtime-lifecycle.ts
|
|
1734
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1735
|
+
import {
|
|
1736
|
+
mkdir as mkdir2,
|
|
1737
|
+
readdir as readdir3,
|
|
1738
|
+
readFile as readFile4,
|
|
1739
|
+
rename,
|
|
1740
|
+
rm as rm2,
|
|
1741
|
+
stat as stat3,
|
|
1742
|
+
writeFile as writeFile2
|
|
1743
|
+
} from "fs/promises";
|
|
1744
|
+
import { join as join4 } from "path";
|
|
1745
|
+
|
|
1746
|
+
// server/utils/harness/local/local-state-lock.ts
|
|
1747
|
+
import { randomUUID } from "crypto";
|
|
1748
|
+
import { mkdir, readFile as readFile3, rm, stat as stat2, writeFile } from "fs/promises";
|
|
1749
|
+
import { join as join3 } from "path";
|
|
1750
|
+
var DEFAULT_STALE_MS = 3e4;
|
|
1751
|
+
var DEFAULT_POLL_MS = 25;
|
|
1752
|
+
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
1753
|
+
async function readLockBody(file) {
|
|
1754
|
+
try {
|
|
1755
|
+
const parsed = JSON.parse(await readFile3(file, "utf8"));
|
|
1756
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1757
|
+
const body = parsed;
|
|
1758
|
+
if (typeof body.pid !== "number" || !Number.isInteger(body.pid) || body.pid <= 0 || typeof body.nonce !== "string" || body.nonce.length === 0) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
return { pid: body.pid, nonce: body.nonce, at: Number(body.at) || 0 };
|
|
1762
|
+
} catch {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
function lockHolderProvablyGone(body) {
|
|
1767
|
+
if (body === null || body.pid === process.pid) return false;
|
|
1768
|
+
try {
|
|
1769
|
+
process.kill(body.pid, 0);
|
|
1770
|
+
return false;
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
return error.code === "ESRCH";
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function createLocalStateMutationLock(options) {
|
|
1776
|
+
let mutationChain = Promise.resolve();
|
|
1777
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
1778
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
1779
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1780
|
+
const acquire = async () => {
|
|
1781
|
+
const root = options.rootDir();
|
|
1782
|
+
const file = join3(root, options.lockFileName);
|
|
1783
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
1784
|
+
const nonce = randomUUID();
|
|
1785
|
+
const deadline = Date.now() + timeoutMs;
|
|
1786
|
+
for (; ; ) {
|
|
1787
|
+
try {
|
|
1788
|
+
await writeFile(
|
|
1789
|
+
file,
|
|
1790
|
+
JSON.stringify({ pid: process.pid, nonce, at: Date.now() }),
|
|
1791
|
+
{ flag: "wx", mode: 384 }
|
|
1792
|
+
);
|
|
1793
|
+
return async () => {
|
|
1794
|
+
const held = await readLockBody(file);
|
|
1795
|
+
if (held?.nonce !== nonce) {
|
|
1796
|
+
logger.warn(
|
|
1797
|
+
`[local-harness] ${options.resourceLabel} lock was taken over; not releasing`
|
|
1798
|
+
);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
await rm(file, { force: true }).catch(() => {
|
|
1802
|
+
});
|
|
1803
|
+
};
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
if (error.code !== "EEXIST") throw error;
|
|
1806
|
+
const body = await readLockBody(file);
|
|
1807
|
+
const age = await stat2(file).then(
|
|
1808
|
+
(info) => Date.now() - info.mtimeMs,
|
|
1809
|
+
() => 0
|
|
1810
|
+
);
|
|
1811
|
+
const holderGone = lockHolderProvablyGone(body);
|
|
1812
|
+
const unattributable = body === null && age > staleMs;
|
|
1813
|
+
if (holderGone || unattributable) {
|
|
1814
|
+
logger.warn(
|
|
1815
|
+
`[local-harness] breaking an abandoned ${options.resourceLabel} lock`,
|
|
1816
|
+
{ age, holderGone, unattributable }
|
|
1817
|
+
);
|
|
1818
|
+
await rm(file, { force: true }).catch(() => {
|
|
1819
|
+
});
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (Date.now() > deadline) {
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
`timed out waiting for the local harness ${options.resourceLabel} lock` + (body === null ? "" : ` (held by pid ${body.pid})`) + ". Its holder could not be proven gone, and taking it over would admit concurrent state writers."
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
await new Promise(
|
|
1828
|
+
(resolvePromise) => setTimeout(resolvePromise, pollMs)
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
return (operation) => {
|
|
1834
|
+
const guarded = async () => {
|
|
1835
|
+
const release = await acquire();
|
|
1836
|
+
try {
|
|
1837
|
+
return await operation();
|
|
1838
|
+
} finally {
|
|
1839
|
+
await release();
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
const run = mutationChain.then(guarded, guarded);
|
|
1843
|
+
mutationChain = run.then(
|
|
1844
|
+
() => void 0,
|
|
1845
|
+
() => void 0
|
|
1846
|
+
);
|
|
1847
|
+
return run;
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// server/utils/harness/local/runtime-lifecycle.ts
|
|
1852
|
+
var OPERATION_FILE = ".mcpjam-operation.json";
|
|
1853
|
+
var USE_DIR = ".mcpjam-inuse";
|
|
1854
|
+
var STAGING_PREFIX = ".mcpjam-tmp-";
|
|
1855
|
+
var STAGING_OWNER_FILE = ".mcpjam-staging-owner.json";
|
|
1856
|
+
function operationKeyString(key) {
|
|
1857
|
+
return [
|
|
1858
|
+
key.runtimeRoot,
|
|
1859
|
+
key.harnessId,
|
|
1860
|
+
key.target,
|
|
1861
|
+
key.packVersion,
|
|
1862
|
+
key.treeDigest
|
|
1863
|
+
].join("\0");
|
|
1864
|
+
}
|
|
1865
|
+
function installRootOf(key) {
|
|
1866
|
+
return join4(key.runtimeRoot, key.target);
|
|
1867
|
+
}
|
|
1868
|
+
var locksByRoot = /* @__PURE__ */ new Map();
|
|
1869
|
+
function lockFor(key) {
|
|
1870
|
+
const root = installRootOf(key);
|
|
1871
|
+
let lock = locksByRoot.get(root);
|
|
1872
|
+
if (lock === void 0) {
|
|
1873
|
+
lock = createLocalStateMutationLock({
|
|
1874
|
+
rootDir: () => root,
|
|
1875
|
+
lockFileName: ".mcpjam-runtime-lifecycle.lock",
|
|
1876
|
+
resourceLabel: "runtime lifecycle",
|
|
1877
|
+
// Short by construction: every critical section here is a read, a
|
|
1878
|
+
// compare and an atomic write. A caller waiting longer than this is
|
|
1879
|
+
// waiting on something that is not doing that.
|
|
1880
|
+
timeoutMs: 15e3
|
|
1881
|
+
});
|
|
1882
|
+
locksByRoot.set(root, lock);
|
|
1883
|
+
}
|
|
1884
|
+
return lock;
|
|
1885
|
+
}
|
|
1886
|
+
function ownerProvablyGone(record) {
|
|
1887
|
+
if (record.ownerPid === process.pid) {
|
|
1888
|
+
return record.ownerStartedAt !== processStartedAt();
|
|
1889
|
+
}
|
|
1890
|
+
try {
|
|
1891
|
+
process.kill(record.ownerPid, 0);
|
|
1892
|
+
return false;
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
return error.code === "ESRCH";
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
var cachedStartedAt = null;
|
|
1898
|
+
function processStartedAt() {
|
|
1899
|
+
if (cachedStartedAt === null) {
|
|
1900
|
+
cachedStartedAt = Math.floor((Date.now() - process.uptime() * 1e3) / 1e3);
|
|
1901
|
+
}
|
|
1902
|
+
return cachedStartedAt;
|
|
1903
|
+
}
|
|
1904
|
+
async function writeJsonAtomic(file, value) {
|
|
1905
|
+
const tmp = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1906
|
+
await writeFile2(tmp, `${JSON.stringify(value, null, 2)}
|
|
1907
|
+
`, { mode: 384 });
|
|
1908
|
+
await rename(tmp, file);
|
|
1909
|
+
}
|
|
1910
|
+
async function readJson(file) {
|
|
1911
|
+
try {
|
|
1912
|
+
return JSON.parse(await readFile4(file, "utf8"));
|
|
1913
|
+
} catch {
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
async function readRuntimeOperation(key) {
|
|
1918
|
+
const file = join4(installRootOf(key), OPERATION_FILE);
|
|
1919
|
+
const record = await readJson(file);
|
|
1920
|
+
if (record === null) return null;
|
|
1921
|
+
if (record.packVersion !== key.packVersion || record.treeDigest !== key.treeDigest || record.harnessId !== key.harnessId) {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
const terminal = record.state === "ready" || record.state === "failed" || record.state === "interrupted";
|
|
1925
|
+
if (terminal || !ownerProvablyGone(record)) return record;
|
|
1926
|
+
return lockFor(key)(async () => {
|
|
1927
|
+
const fresh = await readJson(file);
|
|
1928
|
+
if (fresh === null) return null;
|
|
1929
|
+
if (fresh.attemptId !== record.attemptId) return fresh;
|
|
1930
|
+
if (fresh.state === "ready" || fresh.state === "failed" || fresh.state === "interrupted") {
|
|
1931
|
+
return fresh;
|
|
1932
|
+
}
|
|
1933
|
+
if (!ownerProvablyGone(fresh)) return fresh;
|
|
1934
|
+
const reconciled = {
|
|
1935
|
+
...fresh,
|
|
1936
|
+
state: "interrupted",
|
|
1937
|
+
message: "the Inspector that started this install is no longer running, so the attempt was abandoned partway through",
|
|
1938
|
+
updatedAt: Date.now()
|
|
1939
|
+
};
|
|
1940
|
+
await writeJsonAtomic(file, reconciled);
|
|
1941
|
+
logger.info("[local-harness] recovered an interrupted install attempt", {
|
|
1942
|
+
attemptId: reconciled.attemptId,
|
|
1943
|
+
packVersion: reconciled.packVersion
|
|
1944
|
+
});
|
|
1945
|
+
return reconciled;
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
async function beginInstallAttempt(key) {
|
|
1949
|
+
const root = installRootOf(key);
|
|
1950
|
+
await mkdir2(root, { recursive: true, mode: 448 });
|
|
1951
|
+
const file = join4(root, OPERATION_FILE);
|
|
1952
|
+
return lockFor(key)(async () => {
|
|
1953
|
+
const existing = await readJson(file);
|
|
1954
|
+
if (existing !== null && existing.packVersion === key.packVersion && existing.treeDigest === key.treeDigest && existing.harnessId === key.harnessId) {
|
|
1955
|
+
const live = existing.state !== "ready" && existing.state !== "failed" && existing.state !== "interrupted";
|
|
1956
|
+
if (live && !ownerProvablyGone(existing)) {
|
|
1957
|
+
return { kind: "joined", record: existing };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const record = {
|
|
1961
|
+
attemptId: `att_${randomUUID2()}`,
|
|
1962
|
+
ownerPid: process.pid,
|
|
1963
|
+
ownerStartedAt: processStartedAt(),
|
|
1964
|
+
harnessId: key.harnessId,
|
|
1965
|
+
target: key.target,
|
|
1966
|
+
packVersion: key.packVersion,
|
|
1967
|
+
treeDigest: key.treeDigest,
|
|
1968
|
+
state: "reserved",
|
|
1969
|
+
startedAt: Date.now(),
|
|
1970
|
+
updatedAt: Date.now()
|
|
1971
|
+
};
|
|
1972
|
+
await writeJsonAtomic(file, record);
|
|
1973
|
+
return { kind: "started", record };
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
async function updateInstallAttempt(key, attemptId, patch) {
|
|
1977
|
+
const file = join4(installRootOf(key), OPERATION_FILE);
|
|
1978
|
+
return lockFor(key)(async () => {
|
|
1979
|
+
const existing = await readJson(file);
|
|
1980
|
+
if (existing === null || existing.attemptId !== attemptId) return false;
|
|
1981
|
+
await writeJsonAtomic(file, {
|
|
1982
|
+
...existing,
|
|
1983
|
+
...patch,
|
|
1984
|
+
updatedAt: Date.now()
|
|
1985
|
+
});
|
|
1986
|
+
return true;
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
async function attemptStillOwns(key, attemptId) {
|
|
1990
|
+
const record = await readJson(
|
|
1991
|
+
join4(installRootOf(key), OPERATION_FILE)
|
|
1992
|
+
);
|
|
1993
|
+
return record?.attemptId === attemptId;
|
|
1994
|
+
}
|
|
1995
|
+
async function reserveRuntimeUse(args) {
|
|
1996
|
+
const dir = join4(installRootOf(args.key), USE_DIR);
|
|
1997
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
1998
|
+
const file = join4(dir, `${process.pid}-${randomUUID2()}.json`);
|
|
1999
|
+
const record = {
|
|
2000
|
+
pid: process.pid,
|
|
2001
|
+
startedAt: processStartedAt(),
|
|
2002
|
+
runtimeRoot: args.runtimeRoot,
|
|
2003
|
+
at: Date.now(),
|
|
2004
|
+
...args.label ? { label: args.label } : {}
|
|
2005
|
+
};
|
|
2006
|
+
await writeJsonAtomic(file, record);
|
|
2007
|
+
let released = false;
|
|
2008
|
+
return {
|
|
2009
|
+
release: async () => {
|
|
2010
|
+
if (released) return;
|
|
2011
|
+
released = true;
|
|
2012
|
+
await rm2(file, { force: true }).catch(() => {
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
async function runtimeUseState(args) {
|
|
2018
|
+
const dir = join4(installRootOf(args.key), USE_DIR);
|
|
2019
|
+
let entries;
|
|
2020
|
+
try {
|
|
2021
|
+
entries = await readdir3(dir);
|
|
2022
|
+
} catch {
|
|
2023
|
+
return { busy: false, holders: [] };
|
|
2024
|
+
}
|
|
2025
|
+
const holders = [];
|
|
2026
|
+
for (const entry of entries) {
|
|
2027
|
+
if (!entry.endsWith(".json")) continue;
|
|
2028
|
+
const file = join4(dir, entry);
|
|
2029
|
+
const record = await readJson(file);
|
|
2030
|
+
if (record === null) {
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (record.runtimeRoot !== args.runtimeRoot) continue;
|
|
2034
|
+
if (ownerProvablyGone({ ownerPid: record.pid, ownerStartedAt: record.startedAt })) {
|
|
2035
|
+
await rm2(file, { force: true }).catch(() => {
|
|
2036
|
+
});
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
holders.push(record.pid);
|
|
2040
|
+
}
|
|
2041
|
+
return { busy: holders.length > 0, holders };
|
|
2042
|
+
}
|
|
2043
|
+
async function claimStagingDirectory(args) {
|
|
2044
|
+
await writeJsonAtomic(join4(args.staging, STAGING_OWNER_FILE), {
|
|
2045
|
+
pid: process.pid,
|
|
2046
|
+
startedAt: processStartedAt(),
|
|
2047
|
+
attemptId: args.attemptId,
|
|
2048
|
+
at: Date.now()
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
async function sweepAbandonedStaging(key) {
|
|
2052
|
+
const root = installRootOf(key);
|
|
2053
|
+
let entries;
|
|
2054
|
+
try {
|
|
2055
|
+
entries = await readdir3(root);
|
|
2056
|
+
} catch {
|
|
2057
|
+
return { removed: 0, left: 0 };
|
|
2058
|
+
}
|
|
2059
|
+
let removed = 0;
|
|
2060
|
+
let left = 0;
|
|
2061
|
+
for (const entry of entries) {
|
|
2062
|
+
if (!entry.startsWith(STAGING_PREFIX)) continue;
|
|
2063
|
+
const path = join4(root, entry);
|
|
2064
|
+
const owner = await readJson(join4(path, STAGING_OWNER_FILE));
|
|
2065
|
+
if (owner === null) {
|
|
2066
|
+
left += 1;
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (!ownerProvablyGone({ ownerPid: owner.pid, ownerStartedAt: owner.startedAt })) {
|
|
2070
|
+
left += 1;
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
await rm2(path, { recursive: true, force: true }).catch(() => {
|
|
2074
|
+
});
|
|
2075
|
+
removed += 1;
|
|
2076
|
+
}
|
|
2077
|
+
if (removed > 0 || left > 0) {
|
|
2078
|
+
logger.debug("[local-harness] staging sweep", { removed, left });
|
|
2079
|
+
}
|
|
2080
|
+
return { removed, left };
|
|
2081
|
+
}
|
|
2082
|
+
async function recoverInterruptedActivation(args) {
|
|
2083
|
+
const previous = `${args.versionRoot}${PREVIOUS_SUFFIX}`;
|
|
2084
|
+
const hasPrevious = await stat3(previous).then(
|
|
2085
|
+
(info) => info.isDirectory(),
|
|
2086
|
+
() => false
|
|
2087
|
+
);
|
|
2088
|
+
if (!hasPrevious) return false;
|
|
2089
|
+
return lockFor(args.key)(async () => {
|
|
2090
|
+
const current = await stat3(args.versionRoot).then(
|
|
2091
|
+
(info) => info.isDirectory(),
|
|
2092
|
+
() => false
|
|
2093
|
+
);
|
|
2094
|
+
if (current) {
|
|
2095
|
+
await rm2(previous, { recursive: true, force: true }).catch(() => {
|
|
2096
|
+
});
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
await rename(previous, args.versionRoot);
|
|
2100
|
+
logger.warn("[local-harness] restored a runtime after an interrupted activation", {
|
|
2101
|
+
packVersion: args.key.packVersion
|
|
2102
|
+
});
|
|
2103
|
+
return true;
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
var PREVIOUS_SUFFIX = ".mcpjam-previous";
|
|
2107
|
+
function withRuntimeLifecycleLock(key, operation) {
|
|
2108
|
+
return lockFor(key)(operation);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1420
2111
|
// server/utils/harness/local/runtime-install.ts
|
|
1421
2112
|
var MAX_ARCHIVE_BYTES = 1500 * 1024 * 1024;
|
|
1422
2113
|
var MAX_EXTRACTED_BYTES = 2 * 1024 * 1024 * 1024;
|
|
@@ -1424,13 +2115,13 @@ var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
|
1424
2115
|
function runtimeInstallRoot() {
|
|
1425
2116
|
const override = process.env.MCPJAM_RUNTIME_ROOT;
|
|
1426
2117
|
if (override && override.trim().length > 0) return override.trim();
|
|
1427
|
-
return
|
|
2118
|
+
return join5(homedir(), ".mcpjam", "harness-local", "runtime");
|
|
1428
2119
|
}
|
|
1429
2120
|
function packVersionRoot(packVersion, target = localPackTarget()) {
|
|
1430
|
-
return
|
|
2121
|
+
return join5(targetInstallRoot(target), packVersion);
|
|
1431
2122
|
}
|
|
1432
2123
|
function targetInstallRoot(target) {
|
|
1433
|
-
return
|
|
2124
|
+
return join5(runtimeInstallRoot(), target ?? packPlatformKey());
|
|
1434
2125
|
}
|
|
1435
2126
|
function expectedPackFor(harnessId, target) {
|
|
1436
2127
|
const override = developmentPackExpectation();
|
|
@@ -1466,124 +2157,357 @@ function packPlatformKey(platform = process.platform, arch = process.arch) {
|
|
|
1466
2157
|
return `${platform}-${arch}`;
|
|
1467
2158
|
}
|
|
1468
2159
|
async function readRuntimeInstallStatus(args) {
|
|
1469
|
-
const
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
2160
|
+
const resolved = resolveInstallTarget(args);
|
|
2161
|
+
if (!resolved.ok) return resolved.status;
|
|
2162
|
+
const { key, expected, versionRoot } = resolved;
|
|
2163
|
+
await recoverInterruptedActivation({ key, versionRoot });
|
|
2164
|
+
const inProcess = active.get(operationKeyString(key));
|
|
2165
|
+
if (inProcess !== void 0) return inProcess.status;
|
|
2166
|
+
const record = await readRuntimeOperation(key);
|
|
2167
|
+
if (record !== null && !isRecordTerminal(record)) {
|
|
2168
|
+
return operationRecordStatus(record);
|
|
2169
|
+
}
|
|
2170
|
+
const onDisk = await readMarkerStatus({
|
|
2171
|
+
harnessId: args.harnessId,
|
|
2172
|
+
versionRoot,
|
|
2173
|
+
expected
|
|
2174
|
+
});
|
|
2175
|
+
if (onDisk.state === "ready") return onDisk;
|
|
2176
|
+
if (record !== null && isRecordTerminal(record) && record.state !== "ready") {
|
|
2177
|
+
return operationRecordStatus(record);
|
|
1475
2178
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2179
|
+
return onDisk;
|
|
2180
|
+
}
|
|
2181
|
+
function manifestWithExpectedBundleDigest(manifest, harnessId, target) {
|
|
2182
|
+
if (target === null) return manifest;
|
|
2183
|
+
if (manifest.runtime.source !== "managed-bundle") return manifest;
|
|
2184
|
+
const expected = expectedPackFor(harnessId, target);
|
|
2185
|
+
if (expected === null) return manifest;
|
|
2186
|
+
if (manifest.runtime.bundleDigest[target] === expected.treeDigest) {
|
|
2187
|
+
return manifest;
|
|
1483
2188
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2189
|
+
return {
|
|
2190
|
+
...manifest,
|
|
2191
|
+
runtime: {
|
|
2192
|
+
...manifest.runtime,
|
|
2193
|
+
bundleDigest: {
|
|
2194
|
+
...manifest.runtime.bundleDigest,
|
|
2195
|
+
[target]: expected.treeDigest
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async function readVerifiedRuntimeStatus(args) {
|
|
2201
|
+
const status = await readRuntimeInstallStatus(args);
|
|
2202
|
+
if (status.state !== "ready") return status;
|
|
2203
|
+
const platform = currentLocalPlatform(args.platform ?? process.platform);
|
|
2204
|
+
if (platform === null) return status;
|
|
2205
|
+
const resolvedBundle = await resolveManagedBundle({
|
|
2206
|
+
manifest: manifestWithExpectedBundleDigest(
|
|
2207
|
+
LOCAL_HARNESS_MANIFEST[args.harnessId],
|
|
2208
|
+
args.harnessId,
|
|
2209
|
+
localPackTarget(args.platform, args.arch)
|
|
2210
|
+
),
|
|
2211
|
+
runtimeRoot: status.runtimeRoot,
|
|
2212
|
+
platform,
|
|
2213
|
+
...args.arch ? { arch: args.arch } : {}
|
|
2214
|
+
});
|
|
2215
|
+
if (resolvedBundle.ok) return status;
|
|
2216
|
+
return {
|
|
2217
|
+
state: "corrupt",
|
|
2218
|
+
packVersion: status.packVersion,
|
|
2219
|
+
message: resolvedBundle.message
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
async function readMarkerStatus(args) {
|
|
1487
2223
|
try {
|
|
1488
2224
|
const marker = JSON.parse(
|
|
1489
|
-
await
|
|
2225
|
+
await readFile5(join5(args.versionRoot, INSTALL_MARKER), "utf8")
|
|
1490
2226
|
);
|
|
1491
|
-
if (marker.treeDigest !== expected.treeDigest) {
|
|
2227
|
+
if (marker.treeDigest !== args.expected.treeDigest) {
|
|
1492
2228
|
return {
|
|
1493
2229
|
state: "corrupt",
|
|
1494
|
-
packVersion: expected.packVersion,
|
|
2230
|
+
packVersion: args.expected.packVersion,
|
|
1495
2231
|
message: "the installed runtime pack does not match the digest this Inspector expects; reinstall it"
|
|
1496
2232
|
};
|
|
1497
2233
|
}
|
|
1498
|
-
await
|
|
2234
|
+
await stat4(join5(args.versionRoot, args.harnessId));
|
|
1499
2235
|
return {
|
|
1500
2236
|
state: "ready",
|
|
1501
|
-
packVersion: expected.packVersion,
|
|
1502
|
-
runtimeRoot:
|
|
1503
|
-
digest: expected.treeDigest
|
|
2237
|
+
packVersion: args.expected.packVersion,
|
|
2238
|
+
runtimeRoot: args.versionRoot,
|
|
2239
|
+
digest: args.expected.treeDigest
|
|
1504
2240
|
};
|
|
1505
2241
|
} catch {
|
|
1506
|
-
return { state: "absent", packVersion: expected.packVersion };
|
|
2242
|
+
return { state: "absent", packVersion: args.expected.packVersion };
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function isRecordTerminal(record) {
|
|
2246
|
+
return record.state === "ready" || record.state === "failed" || record.state === "interrupted";
|
|
2247
|
+
}
|
|
2248
|
+
function operationRecordStatus(record) {
|
|
2249
|
+
switch (record.state) {
|
|
2250
|
+
case "reserved":
|
|
2251
|
+
case "downloading":
|
|
2252
|
+
return {
|
|
2253
|
+
state: "downloading",
|
|
2254
|
+
packVersion: record.packVersion,
|
|
2255
|
+
percent: record.percent ?? 0,
|
|
2256
|
+
attemptId: record.attemptId
|
|
2257
|
+
};
|
|
2258
|
+
case "verifying":
|
|
2259
|
+
case "activating":
|
|
2260
|
+
return {
|
|
2261
|
+
state: "verifying",
|
|
2262
|
+
packVersion: record.packVersion,
|
|
2263
|
+
attemptId: record.attemptId
|
|
2264
|
+
};
|
|
2265
|
+
case "failed":
|
|
2266
|
+
return {
|
|
2267
|
+
state: "failed",
|
|
2268
|
+
packVersion: record.packVersion,
|
|
2269
|
+
reason: record.reason ?? "unknown",
|
|
2270
|
+
message: record.message ?? "the install did not complete",
|
|
2271
|
+
attemptId: record.attemptId
|
|
2272
|
+
};
|
|
2273
|
+
case "interrupted":
|
|
2274
|
+
return {
|
|
2275
|
+
state: "interrupted",
|
|
2276
|
+
packVersion: record.packVersion,
|
|
2277
|
+
message: record.message ?? "setup was interrupted before it finished",
|
|
2278
|
+
attemptId: record.attemptId
|
|
2279
|
+
};
|
|
2280
|
+
case "ready":
|
|
2281
|
+
return {
|
|
2282
|
+
state: "ready",
|
|
2283
|
+
packVersion: record.packVersion,
|
|
2284
|
+
runtimeRoot: packVersionRoot(record.packVersion, record.target),
|
|
2285
|
+
digest: record.treeDigest
|
|
2286
|
+
};
|
|
1507
2287
|
}
|
|
1508
2288
|
}
|
|
2289
|
+
function resolveInstallTarget(args) {
|
|
2290
|
+
const platform = currentLocalPlatform(args.platform ?? process.platform);
|
|
2291
|
+
if (platform === null) {
|
|
2292
|
+
return {
|
|
2293
|
+
ok: false,
|
|
2294
|
+
status: {
|
|
2295
|
+
state: "unsupported-platform",
|
|
2296
|
+
message: `${args.platform ?? process.platform} has no local harness runtime`
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
const target = localPackTarget(args.platform, args.arch);
|
|
2301
|
+
const expected = target === null ? null : expectedPackFor(args.harnessId, target);
|
|
2302
|
+
if (expected === null || target === null) {
|
|
2303
|
+
return {
|
|
2304
|
+
ok: false,
|
|
2305
|
+
status: {
|
|
2306
|
+
state: "unsupported-platform",
|
|
2307
|
+
message: `no ${args.harnessId} runtime pack has been built for ${packPlatformKey(args.platform, args.arch)}`
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
return {
|
|
2312
|
+
ok: true,
|
|
2313
|
+
platform,
|
|
2314
|
+
target,
|
|
2315
|
+
expected,
|
|
2316
|
+
versionRoot: packVersionRoot(expected.packVersion, target),
|
|
2317
|
+
key: {
|
|
2318
|
+
runtimeRoot: runtimeInstallRoot(),
|
|
2319
|
+
harnessId: args.harnessId,
|
|
2320
|
+
target,
|
|
2321
|
+
packVersion: expected.packVersion,
|
|
2322
|
+
treeDigest: expected.treeDigest
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
1509
2326
|
var INSTALL_MARKER = ".mcpjam-pack-installed.json";
|
|
1510
2327
|
var active = /* @__PURE__ */ new Map();
|
|
1511
|
-
async function
|
|
1512
|
-
const
|
|
1513
|
-
if (
|
|
2328
|
+
async function startRuntimeInstall(options) {
|
|
2329
|
+
const resolved = resolveInstallTarget(options);
|
|
2330
|
+
if (!resolved.ok) {
|
|
1514
2331
|
return {
|
|
1515
|
-
|
|
1516
|
-
|
|
2332
|
+
kind: "refused",
|
|
2333
|
+
status: resolved.status,
|
|
2334
|
+
reason: resolved.status.state === "unsupported-platform" ? resolved.status.message : "this machine has no runtime pack to install"
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const { key, expected, versionRoot } = resolved;
|
|
2338
|
+
if (options.expectedPack !== void 0 && (options.expectedPack.packVersion !== expected.packVersion || options.expectedPack.treeDigest !== expected.treeDigest)) {
|
|
2339
|
+
return {
|
|
2340
|
+
kind: "refused",
|
|
2341
|
+
reason: "this Inspector now expects a different runtime than the one that was approved, so it will not download it under that approval",
|
|
2342
|
+
status: {
|
|
2343
|
+
state: "failed",
|
|
2344
|
+
packVersion: expected.packVersion,
|
|
2345
|
+
reason: "verification",
|
|
2346
|
+
message: `the approved runtime was ${options.expectedPack.packVersion} (${options.expectedPack.treeDigest.slice(0, 19)}\u2026) but this Inspector now expects ${expected.packVersion} (${expected.treeDigest.slice(0, 19)}\u2026). Authorize the new one.`
|
|
2347
|
+
}
|
|
1517
2348
|
};
|
|
1518
2349
|
}
|
|
1519
|
-
const
|
|
1520
|
-
const
|
|
1521
|
-
if (
|
|
2350
|
+
const keyString = operationKeyString(key);
|
|
2351
|
+
const running = active.get(keyString);
|
|
2352
|
+
if (running !== void 0) {
|
|
2353
|
+
return { kind: "joined", status: running.status };
|
|
2354
|
+
}
|
|
2355
|
+
await recoverInterruptedActivation({ key, versionRoot });
|
|
2356
|
+
const verified = await readVerifiedRuntimeStatus(options);
|
|
2357
|
+
if (verified.state === "ready") return { kind: "ready", status: verified };
|
|
2358
|
+
const attempt = await beginInstallAttempt(key);
|
|
2359
|
+
if (attempt.kind === "joined") {
|
|
1522
2360
|
return {
|
|
1523
|
-
|
|
1524
|
-
|
|
2361
|
+
kind: "joined",
|
|
2362
|
+
status: operationRecordStatus(attempt.record),
|
|
2363
|
+
attemptId: attempt.record.attemptId
|
|
1525
2364
|
};
|
|
1526
2365
|
}
|
|
1527
|
-
const existing = active.get(expected.packVersion);
|
|
1528
|
-
if (existing !== void 0) return existing.promise;
|
|
1529
2366
|
const record = {
|
|
1530
|
-
status: {
|
|
2367
|
+
status: {
|
|
2368
|
+
state: "downloading",
|
|
2369
|
+
packVersion: expected.packVersion,
|
|
2370
|
+
percent: 0,
|
|
2371
|
+
attemptId: attempt.record.attemptId
|
|
2372
|
+
},
|
|
1531
2373
|
promise: Promise.resolve({
|
|
1532
2374
|
state: "absent",
|
|
1533
2375
|
packVersion: expected.packVersion
|
|
1534
2376
|
})
|
|
1535
2377
|
};
|
|
1536
|
-
active.set(
|
|
2378
|
+
active.set(keyString, record);
|
|
2379
|
+
record.promise = runInstallAttempt({
|
|
2380
|
+
options,
|
|
2381
|
+
resolved,
|
|
2382
|
+
attemptId: attempt.record.attemptId,
|
|
2383
|
+
record,
|
|
2384
|
+
keyString
|
|
2385
|
+
});
|
|
2386
|
+
void record.promise;
|
|
2387
|
+
return {
|
|
2388
|
+
kind: "started",
|
|
2389
|
+
status: record.status,
|
|
2390
|
+
attemptId: attempt.record.attemptId
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
async function installRuntimePack(options) {
|
|
2394
|
+
const started = await startRuntimeInstall(options);
|
|
2395
|
+
if (started.kind === "ready" || started.kind === "refused") {
|
|
2396
|
+
return started.status;
|
|
2397
|
+
}
|
|
2398
|
+
const resolved = resolveInstallTarget(options);
|
|
2399
|
+
if (!resolved.ok) return resolved.status;
|
|
2400
|
+
const running = active.get(operationKeyString(resolved.key));
|
|
2401
|
+
if (running !== void 0) return running.promise;
|
|
2402
|
+
return awaitForeignAttempt(resolved.key, options);
|
|
2403
|
+
}
|
|
2404
|
+
async function awaitForeignAttempt(key, options) {
|
|
2405
|
+
for (; ; ) {
|
|
2406
|
+
if (options.signal?.aborted === true) {
|
|
2407
|
+
return readRuntimeInstallStatus(options);
|
|
2408
|
+
}
|
|
2409
|
+
const record = await readRuntimeOperation(key);
|
|
2410
|
+
if (record === null) return readRuntimeInstallStatus(options);
|
|
2411
|
+
if (isRecordTerminal(record)) {
|
|
2412
|
+
return record.state === "ready" ? readRuntimeInstallStatus(options) : operationRecordStatus(record);
|
|
2413
|
+
}
|
|
2414
|
+
options.onProgress?.(operationRecordStatus(record));
|
|
2415
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
async function runInstallAttempt(args) {
|
|
2419
|
+
const { options, resolved, attemptId, record, keyString } = args;
|
|
2420
|
+
const { key, expected } = resolved;
|
|
1537
2421
|
const setStatus = (status) => {
|
|
1538
2422
|
record.status = status;
|
|
1539
2423
|
options.onProgress?.(status);
|
|
2424
|
+
void updateInstallAttempt(key, attemptId, {
|
|
2425
|
+
state: status.state === "downloading" ? "downloading" : status.state === "verifying" ? "verifying" : "reserved",
|
|
2426
|
+
...status.state === "downloading" ? { percent: status.percent } : {}
|
|
2427
|
+
}).catch(() => {
|
|
2428
|
+
});
|
|
1540
2429
|
};
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
const result = await performInstall({
|
|
2432
|
+
...options,
|
|
2433
|
+
platform: resolved.platform,
|
|
2434
|
+
expected,
|
|
2435
|
+
key,
|
|
2436
|
+
attemptId,
|
|
2437
|
+
setStatus
|
|
2438
|
+
});
|
|
2439
|
+
record.status = result;
|
|
2440
|
+
await updateInstallAttempt(key, attemptId, {
|
|
2441
|
+
state: result.state === "ready" ? "ready" : "failed",
|
|
2442
|
+
...result.state === "failed" ? { reason: result.reason, message: result.message } : {}
|
|
2443
|
+
}).catch(() => {
|
|
2444
|
+
});
|
|
2445
|
+
return result;
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
const failure = classifyInstallFailure(error, expected.packVersion);
|
|
2448
|
+
logger.warn("[local-harness] runtime pack install failed", {
|
|
2449
|
+
reason: failure.reason,
|
|
2450
|
+
message: failure.message
|
|
2451
|
+
});
|
|
2452
|
+
record.status = failure;
|
|
2453
|
+
await updateInstallAttempt(key, attemptId, {
|
|
2454
|
+
state: "failed",
|
|
2455
|
+
reason: failure.reason,
|
|
2456
|
+
message: failure.message
|
|
2457
|
+
}).catch(() => {
|
|
2458
|
+
});
|
|
2459
|
+
return failure;
|
|
2460
|
+
} finally {
|
|
2461
|
+
active.delete(keyString);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function classifyInstallFailure(error, packVersion) {
|
|
2465
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2466
|
+
const code = error?.code;
|
|
2467
|
+
const causeCode = error?.cause?.code;
|
|
2468
|
+
const reason = code === "ENOSPC" || code === "EACCES" || code === "EPERM" || code === "EROFS" || code === "EDQUOT" ? "disk" : code === "ENOTFOUND" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNREFUSED" || causeCode === "ENOTFOUND" || causeCode === "ECONNRESET" || causeCode === "ETIMEDOUT" || causeCode === "ECONNREFUSED" || /could not be downloaded|responded \d{3}|fetch failed|network/i.test(
|
|
2469
|
+
message
|
|
2470
|
+
) ? "network" : /does not match|signature|signed manifest|no signed manifest|digest|exceeds its size ceiling|has no .* directory at its root/i.test(
|
|
2471
|
+
message
|
|
2472
|
+
) ? "verification" : "unknown";
|
|
2473
|
+
return { state: "failed", packVersion, reason, message };
|
|
1562
2474
|
}
|
|
1563
2475
|
async function performInstall(args) {
|
|
1564
|
-
const { expected, setStatus } = args;
|
|
2476
|
+
const { expected, setStatus, key, attemptId } = args;
|
|
1565
2477
|
const platformKey = packPlatformKey(args.platform, args.arch);
|
|
1566
2478
|
const target = localPackTarget(args.platform, args.arch);
|
|
1567
2479
|
const versionRoot = packVersionRoot(expected.packVersion, target);
|
|
1568
2480
|
const installRoot = targetInstallRoot(target);
|
|
1569
|
-
await
|
|
1570
|
-
|
|
1571
|
-
|
|
2481
|
+
await mkdir3(installRoot, { recursive: true, mode: 448 });
|
|
2482
|
+
await sweepAbandonedStaging(key);
|
|
2483
|
+
const staging = join5(installRoot, `${STAGING_PREFIX}${randomUUID3()}`);
|
|
2484
|
+
await mkdir3(staging, { recursive: true, mode: 448 });
|
|
2485
|
+
await claimStagingDirectory({ staging, attemptId });
|
|
1572
2486
|
try {
|
|
1573
2487
|
const source = packSourceFor(expected.packVersion, platformKey);
|
|
1574
|
-
const archivePath =
|
|
1575
|
-
setStatus({
|
|
2488
|
+
const archivePath = join5(staging, "pack.tar.gz");
|
|
2489
|
+
setStatus({
|
|
2490
|
+
state: "downloading",
|
|
2491
|
+
packVersion: expected.packVersion,
|
|
2492
|
+
percent: 0,
|
|
2493
|
+
attemptId
|
|
2494
|
+
});
|
|
1576
2495
|
const archiveSha = await fetchArchive({
|
|
1577
2496
|
source,
|
|
1578
2497
|
destination: archivePath,
|
|
1579
2498
|
onPercent: (percent) => setStatus({
|
|
1580
2499
|
state: "downloading",
|
|
1581
2500
|
packVersion: expected.packVersion,
|
|
1582
|
-
percent
|
|
2501
|
+
percent,
|
|
2502
|
+
attemptId
|
|
1583
2503
|
}),
|
|
1584
2504
|
...args.signal ? { signal: args.signal } : {}
|
|
1585
2505
|
});
|
|
1586
|
-
setStatus({
|
|
2506
|
+
setStatus({
|
|
2507
|
+
state: "verifying",
|
|
2508
|
+
packVersion: expected.packVersion,
|
|
2509
|
+
attemptId
|
|
2510
|
+
});
|
|
1587
2511
|
const manifest = await loadAndVerifyManifest({
|
|
1588
2512
|
source,
|
|
1589
2513
|
packVersion: expected.packVersion,
|
|
@@ -1597,8 +2521,8 @@ async function performInstall(args) {
|
|
|
1597
2521
|
);
|
|
1598
2522
|
}
|
|
1599
2523
|
}
|
|
1600
|
-
const extractRoot =
|
|
1601
|
-
await
|
|
2524
|
+
const extractRoot = join5(staging, "extracted");
|
|
2525
|
+
await mkdir3(extractRoot, { recursive: true, mode: 448 });
|
|
1602
2526
|
await extractTar({
|
|
1603
2527
|
file: archivePath,
|
|
1604
2528
|
cwd: extractRoot,
|
|
@@ -1613,8 +2537,8 @@ async function performInstall(args) {
|
|
|
1613
2537
|
preservePaths: false,
|
|
1614
2538
|
strict: true
|
|
1615
2539
|
});
|
|
1616
|
-
const packRoot =
|
|
1617
|
-
const info = await
|
|
2540
|
+
const packRoot = join5(extractRoot, args.harnessId);
|
|
2541
|
+
const info = await stat4(packRoot).catch(() => null);
|
|
1618
2542
|
if (info === null || !info.isDirectory()) {
|
|
1619
2543
|
throw new Error(
|
|
1620
2544
|
`the runtime pack archive has no ${args.harnessId} directory at its root`
|
|
@@ -1633,8 +2557,8 @@ async function performInstall(args) {
|
|
|
1633
2557
|
);
|
|
1634
2558
|
}
|
|
1635
2559
|
if (args.platform === "darwin") await clearQuarantine(extractRoot);
|
|
1636
|
-
await
|
|
1637
|
-
|
|
2560
|
+
await writeFile3(
|
|
2561
|
+
join5(extractRoot, INSTALL_MARKER),
|
|
1638
2562
|
`${JSON.stringify(
|
|
1639
2563
|
{
|
|
1640
2564
|
packVersion: expected.packVersion,
|
|
@@ -1649,10 +2573,18 @@ async function performInstall(args) {
|
|
|
1649
2573
|
`,
|
|
1650
2574
|
{ mode: 384 }
|
|
1651
2575
|
);
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2576
|
+
setStatus({
|
|
2577
|
+
state: "verifying",
|
|
2578
|
+
packVersion: expected.packVersion,
|
|
2579
|
+
attemptId
|
|
2580
|
+
});
|
|
2581
|
+
await updateInstallAttempt(key, attemptId, { state: "activating" });
|
|
2582
|
+
await activateVerifiedPack({
|
|
2583
|
+
key,
|
|
2584
|
+
attemptId,
|
|
2585
|
+
versionRoot,
|
|
2586
|
+
extractRoot
|
|
2587
|
+
});
|
|
1656
2588
|
logger.info("[local-harness] runtime pack installed", {
|
|
1657
2589
|
packVersion: expected.packVersion,
|
|
1658
2590
|
platform: platformKey
|
|
@@ -1664,20 +2596,60 @@ async function performInstall(args) {
|
|
|
1664
2596
|
digest
|
|
1665
2597
|
};
|
|
1666
2598
|
} finally {
|
|
1667
|
-
await
|
|
2599
|
+
await rm3(staging, { recursive: true, force: true }).catch(() => {
|
|
1668
2600
|
});
|
|
1669
2601
|
}
|
|
1670
2602
|
}
|
|
2603
|
+
async function activateVerifiedPack(args) {
|
|
2604
|
+
const { key, versionRoot, extractRoot } = args;
|
|
2605
|
+
if (!await attemptStillOwns(key, args.attemptId)) {
|
|
2606
|
+
throw new Error(
|
|
2607
|
+
"another install took over this runtime while this one was downloading, so it was not activated"
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
const inUse = await runtimeUseState({ key, runtimeRoot: versionRoot });
|
|
2611
|
+
if (inUse.busy) {
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`this runtime is in use by ${inUse.holders.length} running session(s) on this machine, so it was not replaced. Stop them and retry.`
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
await withRuntimeLifecycleLock(key, async () => {
|
|
2617
|
+
const stillFree = await runtimeUseState({ key, runtimeRoot: versionRoot });
|
|
2618
|
+
if (stillFree.busy) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
"a session started using this runtime while it was being replaced, so it was left alone. Stop it and retry."
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const previous = `${versionRoot}${PREVIOUS_SUFFIX}`;
|
|
2624
|
+
await rm3(previous, { recursive: true, force: true }).catch(() => {
|
|
2625
|
+
});
|
|
2626
|
+
const hasCurrent = await stat4(versionRoot).then(
|
|
2627
|
+
() => true,
|
|
2628
|
+
() => false
|
|
2629
|
+
);
|
|
2630
|
+
if (hasCurrent) await rename2(versionRoot, previous);
|
|
2631
|
+
try {
|
|
2632
|
+
await rename2(extractRoot, versionRoot);
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
if (hasCurrent) await rename2(previous, versionRoot).catch(() => {
|
|
2635
|
+
});
|
|
2636
|
+
throw error;
|
|
2637
|
+
}
|
|
2638
|
+
await rm3(previous, { recursive: true, force: true }).catch(() => {
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
clearRuntimeVerificationCache();
|
|
2642
|
+
}
|
|
1671
2643
|
async function assertExtractedSize(root) {
|
|
1672
2644
|
let bytes = 0;
|
|
1673
2645
|
const walk = async (dir) => {
|
|
1674
|
-
for (const entry of await
|
|
1675
|
-
const full =
|
|
2646
|
+
for (const entry of await readdir4(dir, { withFileTypes: true })) {
|
|
2647
|
+
const full = join5(dir, entry.name);
|
|
1676
2648
|
if (entry.isDirectory()) {
|
|
1677
2649
|
await walk(full);
|
|
1678
2650
|
continue;
|
|
1679
2651
|
}
|
|
1680
|
-
bytes += (await
|
|
2652
|
+
bytes += (await stat4(full)).size;
|
|
1681
2653
|
if (bytes > MAX_EXTRACTED_BYTES) {
|
|
1682
2654
|
throw new Error("the extracted runtime pack exceeds its size ceiling");
|
|
1683
2655
|
}
|
|
@@ -1685,31 +2657,6 @@ async function assertExtractedSize(root) {
|
|
|
1685
2657
|
};
|
|
1686
2658
|
await walk(root);
|
|
1687
2659
|
}
|
|
1688
|
-
async function sweepOtherVersions(installRoot, keepVersion) {
|
|
1689
|
-
let entries;
|
|
1690
|
-
try {
|
|
1691
|
-
entries = await readdir3(installRoot);
|
|
1692
|
-
} catch {
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
for (const entry of entries) {
|
|
1696
|
-
if (entry === keepVersion) continue;
|
|
1697
|
-
const path = join2(installRoot, entry);
|
|
1698
|
-
if (entry.startsWith(".mcpjam-tmp-")) {
|
|
1699
|
-
await rm(path, { recursive: true, force: true }).catch(() => {
|
|
1700
|
-
});
|
|
1701
|
-
continue;
|
|
1702
|
-
}
|
|
1703
|
-
if (!/^[\w.-]+$/.test(entry)) continue;
|
|
1704
|
-
const owned = await readFile3(join2(path, INSTALL_MARKER), "utf8").then(
|
|
1705
|
-
() => true,
|
|
1706
|
-
() => false
|
|
1707
|
-
);
|
|
1708
|
-
if (!owned) continue;
|
|
1709
|
-
await rm(path, { recursive: true, force: true }).catch(() => {
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
2660
|
async function clearQuarantine(root) {
|
|
1714
2661
|
const { execFile: execFile3 } = await import("child_process");
|
|
1715
2662
|
await new Promise((resolvePromise) => {
|
|
@@ -1730,8 +2677,8 @@ async function loadAndVerifyManifest(args) {
|
|
|
1730
2677
|
try {
|
|
1731
2678
|
if (args.source.kind === "file") {
|
|
1732
2679
|
const dir = args.source.location.endsWith(".tar.gz") ? args.source.location.slice(0, -basename(args.source.location).length) : args.source.location;
|
|
1733
|
-
manifestBytes = await
|
|
1734
|
-
signature = await
|
|
2680
|
+
manifestBytes = await readFile5(join5(dir, manifestName));
|
|
2681
|
+
signature = await readFile5(join5(dir, signatureName), "utf8");
|
|
1735
2682
|
} else {
|
|
1736
2683
|
const base = args.source.location.slice(
|
|
1737
2684
|
0,
|
|
@@ -1807,7 +2754,7 @@ async function fetchArchive(args) {
|
|
|
1807
2754
|
});
|
|
1808
2755
|
if (args.source.kind === "file") {
|
|
1809
2756
|
const path = args.source.location;
|
|
1810
|
-
const info = await
|
|
2757
|
+
const info = await stat4(path);
|
|
1811
2758
|
total = info.size;
|
|
1812
2759
|
if (total > MAX_ARCHIVE_BYTES) {
|
|
1813
2760
|
throw new Error("the runtime pack archive exceeds its size ceiling");
|
|
@@ -1879,7 +2826,7 @@ export {
|
|
|
1879
2826
|
isOriginCaptureHandled,
|
|
1880
2827
|
captureOriginErrorToSentry,
|
|
1881
2828
|
logger,
|
|
1882
|
-
|
|
2829
|
+
localPackTarget,
|
|
1883
2830
|
LOCAL_HARNESS_POLICY_VERSION,
|
|
1884
2831
|
LOCAL_ISOLATION_POLICY_VERSION,
|
|
1885
2832
|
currentLocalPlatform,
|
|
@@ -1894,9 +2841,18 @@ export {
|
|
|
1894
2841
|
resolveManagedBundle,
|
|
1895
2842
|
resolveSystemInstall,
|
|
1896
2843
|
revalidateRuntime,
|
|
2844
|
+
LOCAL_HARNESS_MANIFEST,
|
|
2845
|
+
localPermissionModeFor,
|
|
2846
|
+
resolveLocalCompatibility,
|
|
2847
|
+
createLocalStateMutationLock,
|
|
2848
|
+
reserveRuntimeUse,
|
|
1897
2849
|
runtimeInstallRoot,
|
|
2850
|
+
expectedPackFor,
|
|
1898
2851
|
readRuntimeInstallStatus,
|
|
2852
|
+
manifestWithExpectedBundleDigest,
|
|
2853
|
+
readVerifiedRuntimeStatus,
|
|
2854
|
+
startRuntimeInstall,
|
|
1899
2855
|
installRuntimePack,
|
|
1900
2856
|
reportLocalHarnessRuntimeStatusInBackground
|
|
1901
2857
|
};
|
|
1902
|
-
//# sourceMappingURL=chunk-
|
|
2858
|
+
//# sourceMappingURL=chunk-5TXZXCUR.js.map
|