@mcpjam/inspector 3.3.5 → 3.4.0
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-BOZKS-GG.js +2 -0
- package/dist/client/assets/exception-autocapture-BbnuQ6YK.js +2 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BJ-PSGAP.js +2 -0
- package/dist/client/assets/{index-C-H-Rua1.js → index-CbyipXXu.js} +8 -2488
- package/dist/client/assets/index-Cd1YrfPX.css +1 -0
- package/dist/client/assets/jszip.min-Bh_hqLQz.js +2 -0
- package/dist/client/assets/posthog-recorder-CestZegQ.js +2 -0
- package/dist/client/assets/surveys-BjqljrVF.js +2 -0
- package/dist/client/assets/web-vitals-Bqx_fCFg.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-GHVMCYUP.js → chunk-BLGJ4PIK.js} +1101 -134
- package/dist/server/harness-install-cli.js +1 -1
- package/dist/server/index.js +96027 -60973
- package/package.json +10 -4
- package/dist/client/assets/dead-clicks-autocapture-Df9Whene.js +0 -2
- package/dist/client/assets/exception-autocapture-J7Yit1Qe.js +0 -3
- package/dist/client/assets/highlighted-body-OFNGDK62-F3kgBmdV.js +0 -2
- package/dist/client/assets/index-BG0jrXch.css +0 -1
- package/dist/client/assets/jszip.min-6UaK1SU2.js +0 -3
- package/dist/client/assets/posthog-recorder-B9ffo2hn.js +0 -109
- package/dist/client/assets/surveys-O8sqQqoR.js +0 -2
- package/dist/client/assets/web-vitals-CKCOd0b9.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.0";
|
|
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";
|
|
@@ -372,7 +380,18 @@ var EXPECTED_PACK_VERSION = "";
|
|
|
372
380
|
|
|
373
381
|
// server/utils/harness/local/pack-signing-key.ts
|
|
374
382
|
import { createPublicKey, verify as edVerify } from "crypto";
|
|
375
|
-
var PACK_SIGNING_KEYS = [
|
|
383
|
+
var PACK_SIGNING_KEYS = [
|
|
384
|
+
{
|
|
385
|
+
// Generated 2026-09-02. The private half went straight into the CI secret
|
|
386
|
+
// `LOCAL_HARNESS_PACK_SIGNING_KEY` and exists nowhere else — there is no
|
|
387
|
+
// copy to lose, and recovery is rotation, not restore.
|
|
388
|
+
keyId: "pack-2026-09",
|
|
389
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
390
|
+
MCowBQYDK2VwAyEAjaZXzr+4k5pJEuzIy5JUU858ksLJgjZv9b9jxmNhV6A=
|
|
391
|
+
-----END PUBLIC KEY-----
|
|
392
|
+
`
|
|
393
|
+
}
|
|
394
|
+
];
|
|
376
395
|
function verifyPackManifestSignature(manifestBytes, signatureBase64, keys = PACK_SIGNING_KEYS) {
|
|
377
396
|
if (keys.length === 0) {
|
|
378
397
|
return {
|
|
@@ -413,7 +432,7 @@ function verifyPackManifestSignature(manifestBytes, signatureBase64, keys = PACK
|
|
|
413
432
|
import { createHash } from "crypto";
|
|
414
433
|
import { execFile as execFile2 } from "child_process";
|
|
415
434
|
import { readdir as readdir2, readFile as readFile2, realpath, stat } from "fs/promises";
|
|
416
|
-
import { dirname, isAbsolute, join, normalize, relative, sep } from "path";
|
|
435
|
+
import { dirname, isAbsolute, join as join2, normalize, relative, sep } from "path";
|
|
417
436
|
|
|
418
437
|
// server/utils/harness/local/targets.ts
|
|
419
438
|
var LOCAL_PACK_TARGETS = /* @__PURE__ */ new Set([
|
|
@@ -437,8 +456,9 @@ function currentLocalPlatform(platform = process.platform) {
|
|
|
437
456
|
}
|
|
438
457
|
|
|
439
458
|
// server/utils/harness/local/process-identity.ts
|
|
440
|
-
import { execFile } from "child_process";
|
|
459
|
+
import { execFile, spawn } from "child_process";
|
|
441
460
|
import { readdir, readFile } from "fs/promises";
|
|
461
|
+
import { join } from "path";
|
|
442
462
|
var PS_TIMEOUT_MS = 3e3;
|
|
443
463
|
function parseLinuxProcStat(raw) {
|
|
444
464
|
const close = raw.lastIndexOf(")");
|
|
@@ -523,12 +543,125 @@ async function probeDarwin(pid) {
|
|
|
523
543
|
identity: `darwin:${parsed.lstart}|${parsed.command}`
|
|
524
544
|
};
|
|
525
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
|
+
}
|
|
526
658
|
async function probeProcess(pid, platform = process.platform) {
|
|
527
659
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
528
660
|
return { state: "unknown", reason: "implausible pid" };
|
|
529
661
|
}
|
|
530
662
|
if (platform === "linux") return probeLinux(pid);
|
|
531
663
|
if (platform === "darwin") return probeDarwin(pid);
|
|
664
|
+
if (platform === "win32") return probeWin32(pid);
|
|
532
665
|
return { state: "unknown", reason: `no liveness primitive on ${platform}` };
|
|
533
666
|
}
|
|
534
667
|
async function readProcessBirthIdentity(pid, platform = process.platform) {
|
|
@@ -629,6 +762,12 @@ async function probeProcessGroup(pid, platform = process.platform) {
|
|
|
629
762
|
if (!Number.isInteger(pid) || pid <= 0) return "unknown";
|
|
630
763
|
if (platform === "linux") return probeLinuxGroup(pid);
|
|
631
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
|
+
}
|
|
632
771
|
return "unknown";
|
|
633
772
|
}
|
|
634
773
|
async function readProcessGroupId(pid, platform = process.platform) {
|
|
@@ -732,7 +871,8 @@ function signalProcessGroup(pid, signal, platform = process.platform) {
|
|
|
732
871
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
733
872
|
try {
|
|
734
873
|
if (platform === "win32") {
|
|
735
|
-
|
|
874
|
+
process.kill(pid, signal);
|
|
875
|
+
return true;
|
|
736
876
|
}
|
|
737
877
|
process.kill(-pid, signal);
|
|
738
878
|
return true;
|
|
@@ -830,7 +970,7 @@ async function digestTreeWithSnapshot(root) {
|
|
|
830
970
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
831
971
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
832
972
|
for (const entry of entries) {
|
|
833
|
-
const full =
|
|
973
|
+
const full = join2(dir, entry.name);
|
|
834
974
|
const rel = relative(root, full).split(sep).join("/");
|
|
835
975
|
if (entry.isSymbolicLink()) {
|
|
836
976
|
throw new Error(
|
|
@@ -968,7 +1108,7 @@ async function resolveManagedBundle(args) {
|
|
|
968
1108
|
};
|
|
969
1109
|
}
|
|
970
1110
|
const policy = manifest.runtime;
|
|
971
|
-
const root =
|
|
1111
|
+
const root = join2(runtimeRoot, policy.bundleName);
|
|
972
1112
|
let canonicalRoot;
|
|
973
1113
|
try {
|
|
974
1114
|
canonicalRoot = await realpath(root);
|
|
@@ -1012,7 +1152,7 @@ async function resolveManagedBundle(args) {
|
|
|
1012
1152
|
}
|
|
1013
1153
|
const digest = verification.digest;
|
|
1014
1154
|
const insideBundle = async (relativePath, label, requireExecutable) => {
|
|
1015
|
-
const full =
|
|
1155
|
+
const full = join2(canonicalRoot, relativePath);
|
|
1016
1156
|
if (!normalize(full).startsWith(canonicalRoot + sep)) {
|
|
1017
1157
|
return {
|
|
1018
1158
|
ok: false,
|
|
@@ -1185,7 +1325,7 @@ async function resolveSystemInstall(opts) {
|
|
|
1185
1325
|
const rejections = [];
|
|
1186
1326
|
for (const dir of searchPaths) {
|
|
1187
1327
|
for (const name of policy.executableNames) {
|
|
1188
|
-
const candidate =
|
|
1328
|
+
const candidate = join2(dir, name);
|
|
1189
1329
|
let canonical;
|
|
1190
1330
|
try {
|
|
1191
1331
|
canonical = await realpath(candidate);
|
|
@@ -1325,7 +1465,7 @@ async function detectSnapshotDrift(snapshot) {
|
|
|
1325
1465
|
const walk = async (dir) => {
|
|
1326
1466
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1327
1467
|
for (const entry of entries) {
|
|
1328
|
-
const full =
|
|
1468
|
+
const full = join2(dir, entry.name);
|
|
1329
1469
|
const rel = relative(snapshot.root, full).split(sep).join("/");
|
|
1330
1470
|
if (entry.isSymbolicLink()) return `a symlink appeared at ${rel}`;
|
|
1331
1471
|
if (entry.isDirectory()) {
|
|
@@ -1406,6 +1546,568 @@ async function revalidateRuntime(runtime) {
|
|
|
1406
1546
|
}
|
|
1407
1547
|
}
|
|
1408
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
|
+
|
|
1409
2111
|
// server/utils/harness/local/runtime-install.ts
|
|
1410
2112
|
var MAX_ARCHIVE_BYTES = 1500 * 1024 * 1024;
|
|
1411
2113
|
var MAX_EXTRACTED_BYTES = 2 * 1024 * 1024 * 1024;
|
|
@@ -1413,13 +2115,13 @@ var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
|
1413
2115
|
function runtimeInstallRoot() {
|
|
1414
2116
|
const override = process.env.MCPJAM_RUNTIME_ROOT;
|
|
1415
2117
|
if (override && override.trim().length > 0) return override.trim();
|
|
1416
|
-
return
|
|
2118
|
+
return join5(homedir(), ".mcpjam", "harness-local", "runtime");
|
|
1417
2119
|
}
|
|
1418
2120
|
function packVersionRoot(packVersion, target = localPackTarget()) {
|
|
1419
|
-
return
|
|
2121
|
+
return join5(targetInstallRoot(target), packVersion);
|
|
1420
2122
|
}
|
|
1421
2123
|
function targetInstallRoot(target) {
|
|
1422
|
-
return
|
|
2124
|
+
return join5(runtimeInstallRoot(), target ?? packPlatformKey());
|
|
1423
2125
|
}
|
|
1424
2126
|
function expectedPackFor(harnessId, target) {
|
|
1425
2127
|
const override = developmentPackExpectation();
|
|
@@ -1455,124 +2157,357 @@ function packPlatformKey(platform = process.platform, arch = process.arch) {
|
|
|
1455
2157
|
return `${platform}-${arch}`;
|
|
1456
2158
|
}
|
|
1457
2159
|
async function readRuntimeInstallStatus(args) {
|
|
1458
|
-
const
|
|
1459
|
-
if (
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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);
|
|
1464
2178
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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;
|
|
1472
2188
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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) {
|
|
1476
2223
|
try {
|
|
1477
2224
|
const marker = JSON.parse(
|
|
1478
|
-
await
|
|
2225
|
+
await readFile5(join5(args.versionRoot, INSTALL_MARKER), "utf8")
|
|
1479
2226
|
);
|
|
1480
|
-
if (marker.treeDigest !== expected.treeDigest) {
|
|
2227
|
+
if (marker.treeDigest !== args.expected.treeDigest) {
|
|
1481
2228
|
return {
|
|
1482
2229
|
state: "corrupt",
|
|
1483
|
-
packVersion: expected.packVersion,
|
|
2230
|
+
packVersion: args.expected.packVersion,
|
|
1484
2231
|
message: "the installed runtime pack does not match the digest this Inspector expects; reinstall it"
|
|
1485
2232
|
};
|
|
1486
2233
|
}
|
|
1487
|
-
await
|
|
2234
|
+
await stat4(join5(args.versionRoot, args.harnessId));
|
|
1488
2235
|
return {
|
|
1489
2236
|
state: "ready",
|
|
1490
|
-
packVersion: expected.packVersion,
|
|
1491
|
-
runtimeRoot:
|
|
1492
|
-
digest: expected.treeDigest
|
|
2237
|
+
packVersion: args.expected.packVersion,
|
|
2238
|
+
runtimeRoot: args.versionRoot,
|
|
2239
|
+
digest: args.expected.treeDigest
|
|
1493
2240
|
};
|
|
1494
2241
|
} catch {
|
|
1495
|
-
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
|
+
};
|
|
2287
|
+
}
|
|
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
|
+
};
|
|
1496
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
|
+
};
|
|
1497
2325
|
}
|
|
1498
2326
|
var INSTALL_MARKER = ".mcpjam-pack-installed.json";
|
|
1499
2327
|
var active = /* @__PURE__ */ new Map();
|
|
1500
|
-
async function
|
|
1501
|
-
const
|
|
1502
|
-
if (
|
|
2328
|
+
async function startRuntimeInstall(options) {
|
|
2329
|
+
const resolved = resolveInstallTarget(options);
|
|
2330
|
+
if (!resolved.ok) {
|
|
1503
2331
|
return {
|
|
1504
|
-
|
|
1505
|
-
|
|
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"
|
|
1506
2335
|
};
|
|
1507
2336
|
}
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
if (expected === null) {
|
|
2337
|
+
const { key, expected, versionRoot } = resolved;
|
|
2338
|
+
if (options.expectedPack !== void 0 && (options.expectedPack.packVersion !== expected.packVersion || options.expectedPack.treeDigest !== expected.treeDigest)) {
|
|
1511
2339
|
return {
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
+
}
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
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") {
|
|
2360
|
+
return {
|
|
2361
|
+
kind: "joined",
|
|
2362
|
+
status: operationRecordStatus(attempt.record),
|
|
2363
|
+
attemptId: attempt.record.attemptId
|
|
1514
2364
|
};
|
|
1515
2365
|
}
|
|
1516
|
-
const existing = active.get(expected.packVersion);
|
|
1517
|
-
if (existing !== void 0) return existing.promise;
|
|
1518
2366
|
const record = {
|
|
1519
|
-
status: {
|
|
2367
|
+
status: {
|
|
2368
|
+
state: "downloading",
|
|
2369
|
+
packVersion: expected.packVersion,
|
|
2370
|
+
percent: 0,
|
|
2371
|
+
attemptId: attempt.record.attemptId
|
|
2372
|
+
},
|
|
1520
2373
|
promise: Promise.resolve({
|
|
1521
2374
|
state: "absent",
|
|
1522
2375
|
packVersion: expected.packVersion
|
|
1523
2376
|
})
|
|
1524
2377
|
};
|
|
1525
|
-
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;
|
|
1526
2421
|
const setStatus = (status) => {
|
|
1527
2422
|
record.status = status;
|
|
1528
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
|
+
});
|
|
1529
2429
|
};
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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 };
|
|
1551
2474
|
}
|
|
1552
2475
|
async function performInstall(args) {
|
|
1553
|
-
const { expected, setStatus } = args;
|
|
2476
|
+
const { expected, setStatus, key, attemptId } = args;
|
|
1554
2477
|
const platformKey = packPlatformKey(args.platform, args.arch);
|
|
1555
2478
|
const target = localPackTarget(args.platform, args.arch);
|
|
1556
2479
|
const versionRoot = packVersionRoot(expected.packVersion, target);
|
|
1557
2480
|
const installRoot = targetInstallRoot(target);
|
|
1558
|
-
await
|
|
1559
|
-
|
|
1560
|
-
|
|
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 });
|
|
1561
2486
|
try {
|
|
1562
2487
|
const source = packSourceFor(expected.packVersion, platformKey);
|
|
1563
|
-
const archivePath =
|
|
1564
|
-
setStatus({
|
|
2488
|
+
const archivePath = join5(staging, "pack.tar.gz");
|
|
2489
|
+
setStatus({
|
|
2490
|
+
state: "downloading",
|
|
2491
|
+
packVersion: expected.packVersion,
|
|
2492
|
+
percent: 0,
|
|
2493
|
+
attemptId
|
|
2494
|
+
});
|
|
1565
2495
|
const archiveSha = await fetchArchive({
|
|
1566
2496
|
source,
|
|
1567
2497
|
destination: archivePath,
|
|
1568
2498
|
onPercent: (percent) => setStatus({
|
|
1569
2499
|
state: "downloading",
|
|
1570
2500
|
packVersion: expected.packVersion,
|
|
1571
|
-
percent
|
|
2501
|
+
percent,
|
|
2502
|
+
attemptId
|
|
1572
2503
|
}),
|
|
1573
2504
|
...args.signal ? { signal: args.signal } : {}
|
|
1574
2505
|
});
|
|
1575
|
-
setStatus({
|
|
2506
|
+
setStatus({
|
|
2507
|
+
state: "verifying",
|
|
2508
|
+
packVersion: expected.packVersion,
|
|
2509
|
+
attemptId
|
|
2510
|
+
});
|
|
1576
2511
|
const manifest = await loadAndVerifyManifest({
|
|
1577
2512
|
source,
|
|
1578
2513
|
packVersion: expected.packVersion,
|
|
@@ -1586,8 +2521,8 @@ async function performInstall(args) {
|
|
|
1586
2521
|
);
|
|
1587
2522
|
}
|
|
1588
2523
|
}
|
|
1589
|
-
const extractRoot =
|
|
1590
|
-
await
|
|
2524
|
+
const extractRoot = join5(staging, "extracted");
|
|
2525
|
+
await mkdir3(extractRoot, { recursive: true, mode: 448 });
|
|
1591
2526
|
await extractTar({
|
|
1592
2527
|
file: archivePath,
|
|
1593
2528
|
cwd: extractRoot,
|
|
@@ -1602,8 +2537,8 @@ async function performInstall(args) {
|
|
|
1602
2537
|
preservePaths: false,
|
|
1603
2538
|
strict: true
|
|
1604
2539
|
});
|
|
1605
|
-
const packRoot =
|
|
1606
|
-
const info = await
|
|
2540
|
+
const packRoot = join5(extractRoot, args.harnessId);
|
|
2541
|
+
const info = await stat4(packRoot).catch(() => null);
|
|
1607
2542
|
if (info === null || !info.isDirectory()) {
|
|
1608
2543
|
throw new Error(
|
|
1609
2544
|
`the runtime pack archive has no ${args.harnessId} directory at its root`
|
|
@@ -1622,8 +2557,8 @@ async function performInstall(args) {
|
|
|
1622
2557
|
);
|
|
1623
2558
|
}
|
|
1624
2559
|
if (args.platform === "darwin") await clearQuarantine(extractRoot);
|
|
1625
|
-
await
|
|
1626
|
-
|
|
2560
|
+
await writeFile3(
|
|
2561
|
+
join5(extractRoot, INSTALL_MARKER),
|
|
1627
2562
|
`${JSON.stringify(
|
|
1628
2563
|
{
|
|
1629
2564
|
packVersion: expected.packVersion,
|
|
@@ -1638,10 +2573,18 @@ async function performInstall(args) {
|
|
|
1638
2573
|
`,
|
|
1639
2574
|
{ mode: 384 }
|
|
1640
2575
|
);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
+
});
|
|
1645
2588
|
logger.info("[local-harness] runtime pack installed", {
|
|
1646
2589
|
packVersion: expected.packVersion,
|
|
1647
2590
|
platform: platformKey
|
|
@@ -1653,20 +2596,60 @@ async function performInstall(args) {
|
|
|
1653
2596
|
digest
|
|
1654
2597
|
};
|
|
1655
2598
|
} finally {
|
|
1656
|
-
await
|
|
2599
|
+
await rm3(staging, { recursive: true, force: true }).catch(() => {
|
|
1657
2600
|
});
|
|
1658
2601
|
}
|
|
1659
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
|
+
}
|
|
1660
2643
|
async function assertExtractedSize(root) {
|
|
1661
2644
|
let bytes = 0;
|
|
1662
2645
|
const walk = async (dir) => {
|
|
1663
|
-
for (const entry of await
|
|
1664
|
-
const full =
|
|
2646
|
+
for (const entry of await readdir4(dir, { withFileTypes: true })) {
|
|
2647
|
+
const full = join5(dir, entry.name);
|
|
1665
2648
|
if (entry.isDirectory()) {
|
|
1666
2649
|
await walk(full);
|
|
1667
2650
|
continue;
|
|
1668
2651
|
}
|
|
1669
|
-
bytes += (await
|
|
2652
|
+
bytes += (await stat4(full)).size;
|
|
1670
2653
|
if (bytes > MAX_EXTRACTED_BYTES) {
|
|
1671
2654
|
throw new Error("the extracted runtime pack exceeds its size ceiling");
|
|
1672
2655
|
}
|
|
@@ -1674,31 +2657,6 @@ async function assertExtractedSize(root) {
|
|
|
1674
2657
|
};
|
|
1675
2658
|
await walk(root);
|
|
1676
2659
|
}
|
|
1677
|
-
async function sweepOtherVersions(installRoot, keepVersion) {
|
|
1678
|
-
let entries;
|
|
1679
|
-
try {
|
|
1680
|
-
entries = await readdir3(installRoot);
|
|
1681
|
-
} catch {
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
for (const entry of entries) {
|
|
1685
|
-
if (entry === keepVersion) continue;
|
|
1686
|
-
const path = join2(installRoot, entry);
|
|
1687
|
-
if (entry.startsWith(".mcpjam-tmp-")) {
|
|
1688
|
-
await rm(path, { recursive: true, force: true }).catch(() => {
|
|
1689
|
-
});
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
if (!/^[\w.-]+$/.test(entry)) continue;
|
|
1693
|
-
const owned = await readFile3(join2(path, INSTALL_MARKER), "utf8").then(
|
|
1694
|
-
() => true,
|
|
1695
|
-
() => false
|
|
1696
|
-
);
|
|
1697
|
-
if (!owned) continue;
|
|
1698
|
-
await rm(path, { recursive: true, force: true }).catch(() => {
|
|
1699
|
-
});
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
2660
|
async function clearQuarantine(root) {
|
|
1703
2661
|
const { execFile: execFile3 } = await import("child_process");
|
|
1704
2662
|
await new Promise((resolvePromise) => {
|
|
@@ -1719,8 +2677,8 @@ async function loadAndVerifyManifest(args) {
|
|
|
1719
2677
|
try {
|
|
1720
2678
|
if (args.source.kind === "file") {
|
|
1721
2679
|
const dir = args.source.location.endsWith(".tar.gz") ? args.source.location.slice(0, -basename(args.source.location).length) : args.source.location;
|
|
1722
|
-
manifestBytes = await
|
|
1723
|
-
signature = await
|
|
2680
|
+
manifestBytes = await readFile5(join5(dir, manifestName));
|
|
2681
|
+
signature = await readFile5(join5(dir, signatureName), "utf8");
|
|
1724
2682
|
} else {
|
|
1725
2683
|
const base = args.source.location.slice(
|
|
1726
2684
|
0,
|
|
@@ -1796,7 +2754,7 @@ async function fetchArchive(args) {
|
|
|
1796
2754
|
});
|
|
1797
2755
|
if (args.source.kind === "file") {
|
|
1798
2756
|
const path = args.source.location;
|
|
1799
|
-
const info = await
|
|
2757
|
+
const info = await stat4(path);
|
|
1800
2758
|
total = info.size;
|
|
1801
2759
|
if (total > MAX_ARCHIVE_BYTES) {
|
|
1802
2760
|
throw new Error("the runtime pack archive exceeds its size ceiling");
|
|
@@ -1868,7 +2826,7 @@ export {
|
|
|
1868
2826
|
isOriginCaptureHandled,
|
|
1869
2827
|
captureOriginErrorToSentry,
|
|
1870
2828
|
logger,
|
|
1871
|
-
|
|
2829
|
+
localPackTarget,
|
|
1872
2830
|
LOCAL_HARNESS_POLICY_VERSION,
|
|
1873
2831
|
LOCAL_ISOLATION_POLICY_VERSION,
|
|
1874
2832
|
currentLocalPlatform,
|
|
@@ -1883,9 +2841,18 @@ export {
|
|
|
1883
2841
|
resolveManagedBundle,
|
|
1884
2842
|
resolveSystemInstall,
|
|
1885
2843
|
revalidateRuntime,
|
|
2844
|
+
LOCAL_HARNESS_MANIFEST,
|
|
2845
|
+
localPermissionModeFor,
|
|
2846
|
+
resolveLocalCompatibility,
|
|
2847
|
+
createLocalStateMutationLock,
|
|
2848
|
+
reserveRuntimeUse,
|
|
1886
2849
|
runtimeInstallRoot,
|
|
2850
|
+
expectedPackFor,
|
|
1887
2851
|
readRuntimeInstallStatus,
|
|
2852
|
+
manifestWithExpectedBundleDigest,
|
|
2853
|
+
readVerifiedRuntimeStatus,
|
|
2854
|
+
startRuntimeInstall,
|
|
1888
2855
|
installRuntimePack,
|
|
1889
2856
|
reportLocalHarnessRuntimeStatusInBackground
|
|
1890
2857
|
};
|
|
1891
|
-
//# sourceMappingURL=chunk-
|
|
2858
|
+
//# sourceMappingURL=chunk-BLGJ4PIK.js.map
|