@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.
Files changed (29) hide show
  1. package/README.md +33 -0
  2. package/bin/launch-workspace.d.mts +17 -0
  3. package/bin/launch-workspace.mjs +74 -0
  4. package/bin/start.js +31 -0
  5. package/dist/client/assets/dead-clicks-autocapture-D8PonUmY.js +2 -0
  6. package/dist/client/assets/exception-autocapture-D8fwMBrA.js +2 -0
  7. package/dist/client/assets/highlighted-body-OFNGDK62-DSAkJIuh.js +2 -0
  8. package/dist/client/assets/index-Cd1YrfPX.css +1 -0
  9. package/dist/client/assets/{index-CPz3J6s3.js → index-DRqGwDPP.js} +8 -2488
  10. package/dist/client/assets/jszip.min-CBm444cl.js +2 -0
  11. package/dist/client/assets/posthog-recorder-Cc0E-JHG.js +2 -0
  12. package/dist/client/assets/surveys-DAzKgVVH.js +2 -0
  13. package/dist/client/assets/web-vitals-BTZc1cda.js +2 -0
  14. package/dist/client/claude-desktop-logo.png +0 -0
  15. package/dist/client/guest-credit-wall.png +0 -0
  16. package/dist/client/index.html +2 -2
  17. package/dist/client/user-testing-empty.png +0 -0
  18. package/dist/server/{chunk-7TOT3ZWN.js → chunk-5TXZXCUR.js} +1089 -133
  19. package/dist/server/harness-install-cli.js +1 -1
  20. package/dist/server/index.js +56359 -26517
  21. package/package.json +10 -4
  22. package/dist/client/assets/dead-clicks-autocapture-BMgZJ0Wv.js +0 -2
  23. package/dist/client/assets/exception-autocapture-HqVjr75I.js +0 -3
  24. package/dist/client/assets/highlighted-body-OFNGDK62-CZgdeyBU.js +0 -2
  25. package/dist/client/assets/index-BArhVpAv.css +0 -1
  26. package/dist/client/assets/jszip.min-DzIFtwMm.js +0 -3
  27. package/dist/client/assets/posthog-recorder-BeJOU0JC.js +0 -109
  28. package/dist/client/assets/surveys-BZ6tGdti.js +0 -2
  29. 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.3.6";
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 readdir3,
350
- readFile as readFile3,
351
- rename,
352
- rm,
353
- stat as stat2,
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 join2 } from "path";
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
- return false;
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 = join(dir, entry.name);
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 = join(runtimeRoot, policy.bundleName);
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 = join(canonicalRoot, relativePath);
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 = join(dir, name);
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 = join(dir, entry.name);
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 join2(homedir(), ".mcpjam", "harness-local", "runtime");
2118
+ return join5(homedir(), ".mcpjam", "harness-local", "runtime");
1428
2119
  }
1429
2120
  function packVersionRoot(packVersion, target = localPackTarget()) {
1430
- return join2(targetInstallRoot(target), packVersion);
2121
+ return join5(targetInstallRoot(target), packVersion);
1431
2122
  }
1432
2123
  function targetInstallRoot(target) {
1433
- return join2(runtimeInstallRoot(), target ?? packPlatformKey());
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 platform = currentLocalPlatform(args.platform ?? process.platform);
1470
- if (platform === null) {
1471
- return {
1472
- state: "unsupported-platform",
1473
- message: `${args.platform ?? process.platform} has no local harness runtime`
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
- const target = localPackTarget(args.platform, args.arch);
1477
- const expected = target === null ? null : expectedPackFor(args.harnessId, target);
1478
- if (expected === null) {
1479
- return {
1480
- state: "unsupported-platform",
1481
- message: `no ${args.harnessId} runtime pack has been built for ${packPlatformKey(args.platform, args.arch)}`
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
- const inFlight = active.get(expected.packVersion);
1485
- if (inFlight !== void 0) return inFlight.status;
1486
- const root = packVersionRoot(expected.packVersion, target);
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 readFile3(join2(root, INSTALL_MARKER), "utf8")
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 stat2(join2(root, args.harnessId));
2234
+ await stat4(join5(args.versionRoot, args.harnessId));
1499
2235
  return {
1500
2236
  state: "ready",
1501
- packVersion: expected.packVersion,
1502
- runtimeRoot: root,
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 installRuntimePack(options) {
1512
- const platform = currentLocalPlatform(options.platform ?? process.platform);
1513
- if (platform === null) {
2328
+ async function startRuntimeInstall(options) {
2329
+ const resolved = resolveInstallTarget(options);
2330
+ if (!resolved.ok) {
1514
2331
  return {
1515
- state: "unsupported-platform",
1516
- message: `${options.platform ?? process.platform} has no local harness runtime`
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 target = localPackTarget(options.platform, options.arch);
1520
- const expected = target === null ? null : expectedPackFor(options.harnessId, target);
1521
- if (expected === null) {
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
- state: "unsupported-platform",
1524
- message: `no ${options.harnessId} runtime pack has been built for ${packPlatformKey(options.platform, options.arch)}`
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: { state: "downloading", packVersion: expected.packVersion, percent: 0 },
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(expected.packVersion, record);
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
- record.promise = (async () => {
1542
- try {
1543
- return await performInstall({
1544
- ...options,
1545
- platform,
1546
- expected,
1547
- setStatus
1548
- });
1549
- } catch (error) {
1550
- const message = error instanceof Error ? error.message : String(error);
1551
- logger.warn("[local-harness] runtime pack install failed", { message });
1552
- return {
1553
- state: "corrupt",
1554
- packVersion: expected.packVersion,
1555
- message
1556
- };
1557
- } finally {
1558
- active.delete(expected.packVersion);
1559
- }
1560
- })();
1561
- return record.promise;
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 mkdir(installRoot, { recursive: true, mode: 448 });
1570
- const staging = join2(installRoot, `.mcpjam-tmp-${randomUUID()}`);
1571
- await mkdir(staging, { recursive: true, mode: 448 });
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 = join2(staging, "pack.tar.gz");
1575
- setStatus({ state: "downloading", packVersion: expected.packVersion, percent: 0 });
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({ state: "verifying", packVersion: expected.packVersion });
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 = join2(staging, "extracted");
1601
- await mkdir(extractRoot, { recursive: true, mode: 448 });
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 = join2(extractRoot, args.harnessId);
1617
- const info = await stat2(packRoot).catch(() => null);
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 writeFile(
1637
- join2(extractRoot, INSTALL_MARKER),
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
- await rm(versionRoot, { recursive: true, force: true });
1653
- await rename(extractRoot, versionRoot);
1654
- clearRuntimeVerificationCache();
1655
- await sweepOtherVersions(installRoot, expected.packVersion);
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 rm(staging, { recursive: true, force: true }).catch(() => {
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 readdir3(dir, { withFileTypes: true })) {
1675
- const full = join2(dir, entry.name);
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 stat2(full)).size;
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 readFile3(join2(dir, manifestName));
1734
- signature = await readFile3(join2(dir, signatureName), "utf8");
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 stat2(path);
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
- PACK_TREE_DIGESTS,
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-7TOT3ZWN.js.map
2858
+ //# sourceMappingURL=chunk-5TXZXCUR.js.map