@mcpjam/inspector 3.12.3 → 3.12.5

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 (37) hide show
  1. package/README.md +29 -21
  2. package/bin/access-link.mjs +53 -0
  3. package/bin/start.js +47 -14
  4. package/dist/client/assets/OAuthDesktopReturnNotice-B_x9xTE-.css +2 -0
  5. package/dist/client/assets/OAuthDesktopReturnNotice-LlC451lX.js +11 -0
  6. package/dist/client/assets/{app-bootstrap-C1Jn01kP.js → app-bootstrap-2jatN7gw.js} +376 -374
  7. package/dist/client/assets/{button-RD2Dghjp.js → button-B2OH2ei2.js} +2 -2
  8. package/dist/client/assets/{chunk-YOKDWASO-DTTU1wSE.js → chunk-YOKDWASO-uJ3KOQV7.js} +5 -5
  9. package/dist/client/assets/{dead-clicks-autocapture-CDkbT21a.js → dead-clicks-autocapture-D055GjVp.js} +2 -2
  10. package/dist/client/assets/{error-reporting-Dg5b1JwS.js → error-reporting-DGP36KLx.js} +57 -57
  11. package/dist/client/assets/{error-reporting-_SEhX6IL.js → error-reporting-FWZKbZxG.js} +1 -1
  12. package/dist/client/assets/{exception-autocapture-CtxHwPtn.js → exception-autocapture-103gdJjL.js} +2 -2
  13. package/dist/client/assets/{failed-request-tracker-kUaJ-sVM.js → failed-request-tracker-Cp9_VFMm.js} +2 -2
  14. package/dist/client/assets/{highlighted-body-KPVGNVTW-CBC2hHHb.js → highlighted-body-KPVGNVTW-Dx70h1nm.js} +2 -2
  15. package/dist/client/assets/index-rskHnM3O.js +4 -0
  16. package/dist/client/assets/{jszip.min-B3Qii_az.js → jszip.min-CwJyI5tb.js} +2 -2
  17. package/dist/client/assets/{mermaid-HWGCJPDP-BzT3-9sO.js → mermaid-HWGCJPDP-Ck3prJKl.js} +1 -1
  18. package/dist/client/assets/{posthog-recorder-B06ULcLl.js → posthog-recorder-tKuMFijV.js} +2 -2
  19. package/dist/client/assets/{preload-helper-DvWwGI6x.js → preload-helper-DrcoTQ49.js} +2 -2
  20. package/dist/client/assets/{rolldown-runtime-DVMSi0Yr.js → rolldown-runtime-7FnpGOz6.js} +1 -1
  21. package/dist/client/assets/{surveys-D941hgn3.js → surveys-BsZYxM6N.js} +2 -2
  22. package/dist/client/assets/{trace-timeline-Bk3hAXNq.js → trace-timeline-BOZvHcOG.js} +1 -1
  23. package/dist/client/assets/{trace-timeline-DVZqlQSW.js → trace-timeline-Bf9Oai70.js} +8 -8
  24. package/dist/client/assets/{web-vitals-DZUBt4dL.js → web-vitals-DdzaVN3H.js} +2 -2
  25. package/dist/client/index.html +7 -7
  26. package/dist/server/chunk-3EDQFXTF.js +4801 -0
  27. package/dist/server/{chunk-5RE7KJ2M.js → chunk-4PYR7GMQ.js} +16 -197
  28. package/dist/server/{chunk-WGJHSGJP.js → chunk-QA6VJT6A.js} +961 -963
  29. package/dist/server/chunk-SH24KJBI.js +194 -0
  30. package/dist/server/{consent-lifetime-PTGAZQSU.js → consent-lifetime-M7PS6RUU.js} +3 -2
  31. package/dist/server/harness-install-cli.js +1 -1
  32. package/dist/server/index.js +15110 -16012
  33. package/dist/server/local-turn-XXOSZHMT.js +12 -0
  34. package/package.json +14 -11
  35. package/dist/client/assets/OAuthDesktopReturnNotice-C5D90OSm.js +0 -11
  36. package/dist/client/assets/OAuthDesktopReturnNotice-C95FI7VH.css +0 -2
  37. package/dist/client/assets/index-yOjxp36p.js +0 -4
@@ -37,7 +37,7 @@ function resolveEnvironment() {
37
37
  }
38
38
  return "dev";
39
39
  }
40
- var BAKED_VERSION = "3.12.3";
40
+ var BAKED_VERSION = "3.12.5";
41
41
  function blankToNull(value) {
42
42
  return value === void 0 || value.trim() === "" ? null : value;
43
43
  }
@@ -367,91 +367,6 @@ function emit(eventName, base, payload, options) {
367
367
  }
368
368
  process.on("beforeExit", () => logger.flush());
369
369
 
370
- // server/utils/harness/local/runtime-install.ts
371
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
372
- import { createReadStream, createWriteStream } from "fs";
373
- import {
374
- mkdir as mkdir3,
375
- readdir as readdir4,
376
- readFile as readFile5,
377
- rename as rename2,
378
- rm as rm3,
379
- stat as stat4,
380
- writeFile as writeFile3
381
- } from "fs/promises";
382
- import { homedir } from "os";
383
- import { basename, join as join5 } from "path";
384
- import { pipeline } from "stream/promises";
385
- import { Readable } from "stream";
386
- import { x as extractTar } from "tar";
387
-
388
- // server/utils/harness/local/pack-digests.generated.ts
389
- var PACK_TREE_DIGESTS = {
390
- "claude-code": {},
391
- codex: {}
392
- };
393
- var PACK_RECORDS = {
394
- "claude-code": {},
395
- codex: {}
396
- };
397
- var EXPECTED_PACK_VERSION = "";
398
-
399
- // server/utils/harness/local/pack-signing-key.ts
400
- import { createPublicKey, verify as edVerify } from "crypto";
401
- var PACK_SIGNING_KEYS = [
402
- {
403
- // Generated 2026-09-02. The private half went straight into the CI secret
404
- // `LOCAL_HARNESS_PACK_SIGNING_KEY` and exists nowhere else — there is no
405
- // copy to lose, and recovery is rotation, not restore.
406
- keyId: "pack-2026-09",
407
- publicKeyPem: `-----BEGIN PUBLIC KEY-----
408
- MCowBQYDK2VwAyEAjaZXzr+4k5pJEuzIy5JUU858ksLJgjZv9b9jxmNhV6A=
409
- -----END PUBLIC KEY-----
410
- `
411
- }
412
- ];
413
- function verifyPackManifestSignature(manifestBytes, signatureBase64, keys = PACK_SIGNING_KEYS) {
414
- if (keys.length === 0) {
415
- return {
416
- ok: false,
417
- reason: "no-keys",
418
- message: "this Inspector build carries no runtime pack signing key, so it cannot verify that a downloaded pack came from MCPJam. Packs are installable from a local path for development only."
419
- };
420
- }
421
- let signature;
422
- try {
423
- signature = Buffer.from(signatureBase64.trim(), "base64");
424
- if (signature.length === 0) throw new Error("empty");
425
- } catch {
426
- return {
427
- ok: false,
428
- reason: "malformed",
429
- message: "the pack manifest signature is not valid base64"
430
- };
431
- }
432
- for (const key of keys) {
433
- try {
434
- const publicKey = createPublicKey(key.publicKeyPem);
435
- if (edVerify(null, manifestBytes, publicKey, signature)) {
436
- return { ok: true, keyId: key.keyId };
437
- }
438
- } catch {
439
- continue;
440
- }
441
- }
442
- return {
443
- ok: false,
444
- reason: "bad-signature",
445
- message: "the runtime pack manifest is not signed by any key this Inspector trusts, so the pack is refused"
446
- };
447
- }
448
-
449
- // server/utils/harness/local/runtime-identity.ts
450
- import { createHash } from "crypto";
451
- import { execFile as execFile2 } from "child_process";
452
- import { readdir as readdir2, readFile as readFile2, realpath, stat } from "fs/promises";
453
- import { dirname, isAbsolute, join as join2, normalize, relative, sep } from "path";
454
-
455
370
  // server/utils/harness/local/targets.ts
456
371
  var LOCAL_PACK_TARGETS = /* @__PURE__ */ new Set([
457
372
  "darwin-arm64",
@@ -464,7 +379,7 @@ function localPackTarget(platform = process.platform, arch = process.arch) {
464
379
  const key = `${platform}-${arch}`;
465
380
  return LOCAL_PACK_TARGETS.has(key) ? key : null;
466
381
  }
467
- var LOCAL_HARNESS_POLICY_VERSION = "local-harness-policy-2026-09-01";
382
+ var LOCAL_HARNESS_POLICY_VERSION = "local-harness-policy-2026-09-28";
468
383
  var LOCAL_ISOLATION_POLICY_VERSION = "local-isolation-policy-2026-09-01";
469
384
  function currentLocalPlatform(platform = process.platform) {
470
385
  if (platform === "darwin" || platform === "linux" || platform === "win32") {
@@ -473,446 +388,909 @@ function currentLocalPlatform(platform = process.platform) {
473
388
  return null;
474
389
  }
475
390
 
476
- // server/utils/harness/local/process-identity.ts
477
- import { execFile, spawn } from "child_process";
478
- import { readdir, readFile } from "fs/promises";
391
+ // server/utils/harness/local/runtime-lifecycle.ts
392
+ import { randomUUID as randomUUID2 } from "crypto";
393
+ import {
394
+ mkdir as mkdir2,
395
+ readdir,
396
+ readFile as readFile2,
397
+ rename,
398
+ rm as rm2,
399
+ stat as stat2,
400
+ writeFile as writeFile2
401
+ } from "fs/promises";
402
+ import { join as join2 } from "path";
403
+
404
+ // server/utils/harness/local/local-state-lock.ts
405
+ import { randomUUID } from "crypto";
406
+ import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
479
407
  import { join } from "path";
480
- var PS_TIMEOUT_MS = 3e3;
481
- function parseLinuxProcStat(raw) {
482
- const close = raw.lastIndexOf(")");
483
- if (close === -1) return null;
484
- const fields = raw.slice(close + 2).split(" ");
485
- const state = fields[0];
486
- const starttime = fields[19];
487
- if (!state || !/^[A-Za-z]$/.test(state)) return null;
488
- if (!starttime || !/^\d+$/.test(starttime)) return null;
489
- return { state, starttime };
490
- }
491
- function isDeadState(state) {
492
- return state === "Z" || state === "X" || state === "x";
408
+ var DEFAULT_STALE_MS = 3e4;
409
+ var DEFAULT_POLL_MS = 25;
410
+ var DEFAULT_TIMEOUT_MS = 45e3;
411
+ async function readLockBody(file) {
412
+ try {
413
+ const parsed = JSON.parse(await readFile(file, "utf8"));
414
+ if (!parsed || typeof parsed !== "object") return null;
415
+ const body = parsed;
416
+ if (typeof body.pid !== "number" || !Number.isInteger(body.pid) || body.pid <= 0 || typeof body.nonce !== "string" || body.nonce.length === 0) {
417
+ return null;
418
+ }
419
+ return { pid: body.pid, nonce: body.nonce, at: Number(body.at) || 0 };
420
+ } catch {
421
+ return null;
422
+ }
493
423
  }
494
- async function probeLinux(pid) {
495
- let raw;
424
+ function lockHolderProvablyGone(body) {
425
+ if (body === null || body.pid === process.pid) return false;
496
426
  try {
497
- raw = await readFile(`/proc/${pid}/stat`, "utf8");
427
+ process.kill(body.pid, 0);
428
+ return false;
498
429
  } catch (error) {
499
- const code = error.code;
500
- if (code === "ENOENT" || code === "ESRCH") return { state: "gone" };
501
- return {
502
- state: "unknown",
503
- reason: `/proc read failed (${code ?? "unknown"})`
504
- };
505
- }
506
- const parsed = parseLinuxProcStat(raw);
507
- if (parsed === null) {
508
- return { state: "unknown", reason: "unparseable /proc stat line" };
430
+ return error.code === "ESRCH";
509
431
  }
510
- if (isDeadState(parsed.state)) return { state: "gone" };
511
- return { state: "alive", identity: `linux:${parsed.starttime}` };
512
- }
513
- function parseDarwinPsLine(raw) {
514
- const line = raw.trim();
515
- if (line.length === 0) return null;
516
- const match = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\s\S]+)$/.exec(line);
517
- if (match === null) return null;
518
- const state = match[1];
519
- const lstart = [match[2], match[3], match[4], match[5], match[6]].join(" ");
520
- const command = match[7];
521
- return { state, lstart, command };
522
432
  }
523
- async function probeDarwin(pid) {
524
- const result = await new Promise((resolve) => {
525
- execFile(
526
- "/bin/ps",
527
- ["-o", "state=,lstart=,command=", "-p", String(pid)],
528
- {
529
- timeout: PS_TIMEOUT_MS,
530
- maxBuffer: 16 * 1024,
531
- encoding: "utf8",
532
- env: {}
533
- },
534
- (error, out) => {
535
- if (!error) {
536
- resolve({ ok: true, stdout: typeof out === "string" ? out : "" });
537
- return;
433
+ function createLocalStateMutationLock(options) {
434
+ let mutationChain = Promise.resolve();
435
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
436
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
437
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
438
+ const acquire = async () => {
439
+ const root = options.rootDir();
440
+ const file = join(root, options.lockFileName);
441
+ await mkdir(root, { recursive: true, mode: 448 });
442
+ const nonce = randomUUID();
443
+ const deadline = Date.now() + timeoutMs;
444
+ for (; ; ) {
445
+ try {
446
+ await writeFile(
447
+ file,
448
+ JSON.stringify({ pid: process.pid, nonce, at: Date.now() }),
449
+ { flag: "wx", mode: 384 }
450
+ );
451
+ return async () => {
452
+ const held = await readLockBody(file);
453
+ if (held?.nonce !== nonce) {
454
+ logger.warn(
455
+ `[local-harness] ${options.resourceLabel} lock was taken over; not releasing`
456
+ );
457
+ return;
458
+ }
459
+ await rm(file, { force: true }).catch(() => {
460
+ });
461
+ };
462
+ } catch (error) {
463
+ if (error.code !== "EEXIST") throw error;
464
+ const body = await readLockBody(file);
465
+ const age = await stat(file).then(
466
+ (info) => Date.now() - info.mtimeMs,
467
+ () => 0
468
+ );
469
+ const holderGone = lockHolderProvablyGone(body);
470
+ const unattributable = body === null && age > staleMs;
471
+ if (holderGone || unattributable) {
472
+ logger.warn(
473
+ `[local-harness] breaking an abandoned ${options.resourceLabel} lock`,
474
+ { age, holderGone, unattributable }
475
+ );
476
+ await rm(file, { force: true }).catch(() => {
477
+ });
478
+ continue;
538
479
  }
539
- const err = error;
540
- resolve({
541
- ok: false,
542
- // `ps` exits 1 when no process matches — that IS an answer. A kill
543
- // (timeout) or a spawn failure is not.
544
- exitCode: typeof err.code === "number" ? err.code : null,
545
- reason: err.killed ? "ps timed out" : `ps failed (${String(err.code ?? "unknown")})`
546
- });
480
+ if (Date.now() > deadline) {
481
+ throw new Error(
482
+ `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."
483
+ );
484
+ }
485
+ await new Promise(
486
+ (resolvePromise) => setTimeout(resolvePromise, pollMs)
487
+ );
488
+ }
489
+ }
490
+ };
491
+ return (operation) => {
492
+ const guarded = async () => {
493
+ const release = await acquire();
494
+ try {
495
+ return await operation();
496
+ } finally {
497
+ await release();
547
498
  }
499
+ };
500
+ const run = mutationChain.then(guarded, guarded);
501
+ mutationChain = run.then(
502
+ () => void 0,
503
+ () => void 0
548
504
  );
549
- });
550
- if (!result.ok) {
551
- if (result.exitCode === 1) return { state: "gone" };
552
- return { state: "unknown", reason: result.reason };
553
- }
554
- const parsed = parseDarwinPsLine(result.stdout);
555
- if (parsed === null) {
556
- return result.stdout.trim().length === 0 ? { state: "gone" } : { state: "unknown", reason: "unparseable ps output" };
557
- }
558
- if (isDeadState(parsed.state.charAt(0))) return { state: "gone" };
559
- return {
560
- state: "alive",
561
- identity: `darwin:${parsed.lstart}|${parsed.command}`
505
+ return run;
562
506
  };
563
507
  }
564
- var WIN32_PROBE_TIMEOUT_MS = 15e3;
565
- function win32SystemRoot() {
566
- return process.env.SYSTEMROOT ?? process.env.WINDIR ?? "C:\\Windows";
567
- }
568
- function win32PowerShellPath() {
569
- return join(
570
- win32SystemRoot(),
571
- "System32",
572
- "WindowsPowerShell",
573
- "v1.0",
574
- "powershell.exe"
575
- );
508
+
509
+ // server/utils/harness/local/runtime-lifecycle.ts
510
+ var OPERATION_FILE = ".mcpjam-operation.json";
511
+ var USE_DIR = ".mcpjam-inuse";
512
+ var STAGING_PREFIX = ".mcpjam-tmp-";
513
+ var STAGING_OWNER_FILE = ".mcpjam-staging-owner.json";
514
+ function operationKeyString(key) {
515
+ return [
516
+ key.runtimeRoot,
517
+ key.harnessId,
518
+ key.target,
519
+ key.packVersion,
520
+ key.treeDigest
521
+ ].join("\0");
576
522
  }
577
- function parseWin32ProbeLine(raw) {
578
- const line = raw.trim();
579
- const match = /^(\d+)\|(.+)$/.exec(line);
580
- if (match === null) return null;
581
- return { fileTime: match[1], name: match[2] };
523
+ function installRootOf(key) {
524
+ return join2(key.runtimeRoot, key.target);
582
525
  }
583
- async function probeWin32(pid) {
526
+ var locksByRoot = /* @__PURE__ */ new Map();
527
+ function lockFor(key) {
528
+ const root = installRootOf(key);
529
+ let lock = locksByRoot.get(root);
530
+ if (lock === void 0) {
531
+ lock = createLocalStateMutationLock({
532
+ rootDir: () => root,
533
+ lockFileName: ".mcpjam-runtime-lifecycle.lock",
534
+ resourceLabel: "runtime lifecycle",
535
+ // Short by construction: every critical section here is a read, a
536
+ // compare and an atomic write. A caller waiting longer than this is
537
+ // waiting on something that is not doing that.
538
+ timeoutMs: 15e3
539
+ });
540
+ locksByRoot.set(root, lock);
541
+ }
542
+ return lock;
543
+ }
544
+ function ownerProvablyGone(record) {
545
+ if (record.ownerPid === process.pid) {
546
+ return record.ownerStartedAt !== processStartedAt();
547
+ }
584
548
  try {
585
- process.kill(pid, 0);
549
+ process.kill(record.ownerPid, 0);
550
+ return false;
586
551
  } catch (error) {
587
- const code = error.code;
588
- if (code === "ESRCH") return { state: "gone" };
589
- if (code !== "EPERM") {
590
- return {
591
- state: "unknown",
592
- reason: `kill(0) failed (${code ?? "unknown"})`
593
- };
594
- }
552
+ return error.code === "ESRCH";
595
553
  }
596
- const script = [
597
- `$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue`,
598
- "if ($null -eq $p) { exit 1 }",
599
- "try { $t = $p.StartTime.ToFileTimeUtc() } catch { exit 3 }",
600
- "Write-Output ('{0}|{1}' -f $t, $p.ProcessName)"
601
- ].join("; ");
602
- const result = await new Promise((resolve) => {
603
- let settled = false;
604
- let timer;
605
- const done = (value) => {
606
- if (settled) return;
607
- settled = true;
608
- if (timer !== void 0) clearTimeout(timer);
609
- resolve(value);
610
- };
611
- let child;
612
- try {
613
- child = spawn(
614
- win32PowerShellPath(),
615
- [
616
- "-NoProfile",
617
- "-NonInteractive",
618
- "-InputFormat",
619
- "None",
620
- "-ExecutionPolicy",
621
- "Bypass",
622
- "-Command",
623
- script
624
- ],
625
- { stdio: ["ignore", "pipe", "pipe"], env: process.env, windowsHide: true }
626
- );
627
- } catch (error) {
628
- done({
629
- ok: false,
630
- exitCode: null,
631
- reason: `PowerShell failed to start (${String(
632
- error.code ?? "unknown"
633
- )})`
634
- });
635
- return;
554
+ }
555
+ var cachedStartedAt = null;
556
+ function processStartedAt() {
557
+ if (cachedStartedAt === null) {
558
+ cachedStartedAt = Math.floor((Date.now() - process.uptime() * 1e3) / 1e3);
559
+ }
560
+ return cachedStartedAt;
561
+ }
562
+ async function writeJsonAtomic(file, value) {
563
+ const tmp = `${file}.${process.pid}.${randomUUID2()}.tmp`;
564
+ await writeFile2(tmp, `${JSON.stringify(value, null, 2)}
565
+ `, { mode: 384 });
566
+ await rename(tmp, file);
567
+ }
568
+ async function readJson(file) {
569
+ try {
570
+ return JSON.parse(await readFile2(file, "utf8"));
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ async function readRuntimeOperation(key) {
576
+ const file = join2(installRootOf(key), OPERATION_FILE);
577
+ const record = await readJson(file);
578
+ if (record === null) return null;
579
+ if (record.packVersion !== key.packVersion || record.treeDigest !== key.treeDigest || record.harnessId !== key.harnessId) {
580
+ return null;
581
+ }
582
+ const terminal = record.state === "ready" || record.state === "failed" || record.state === "interrupted";
583
+ if (terminal || !ownerProvablyGone(record)) return record;
584
+ return lockFor(key)(async () => {
585
+ const fresh = await readJson(file);
586
+ if (fresh === null) return null;
587
+ if (fresh.attemptId !== record.attemptId) return fresh;
588
+ if (fresh.state === "ready" || fresh.state === "failed" || fresh.state === "interrupted") {
589
+ return fresh;
636
590
  }
637
- let stdout = "";
638
- child.stdout?.setEncoding("utf8");
639
- child.stdout?.on("data", (chunk) => {
640
- if (stdout.length < 16 * 1024) stdout += chunk;
591
+ if (!ownerProvablyGone(fresh)) return fresh;
592
+ const reconciled = {
593
+ ...fresh,
594
+ state: "interrupted",
595
+ message: "the Inspector that started this install is no longer running, so the attempt was abandoned partway through",
596
+ updatedAt: Date.now()
597
+ };
598
+ await writeJsonAtomic(file, reconciled);
599
+ logger.info("[local-harness] recovered an interrupted install attempt", {
600
+ attemptId: reconciled.attemptId,
601
+ packVersion: reconciled.packVersion
641
602
  });
642
- child.stderr?.resume();
643
- timer = setTimeout(() => {
644
- try {
645
- child.kill();
646
- } catch {
603
+ return reconciled;
604
+ });
605
+ }
606
+ async function beginInstallAttempt(key) {
607
+ const root = installRootOf(key);
608
+ await mkdir2(root, { recursive: true, mode: 448 });
609
+ const file = join2(root, OPERATION_FILE);
610
+ return lockFor(key)(async () => {
611
+ const existing = await readJson(file);
612
+ if (existing !== null && existing.packVersion === key.packVersion && existing.treeDigest === key.treeDigest && existing.harnessId === key.harnessId) {
613
+ const live = existing.state !== "ready" && existing.state !== "failed" && existing.state !== "interrupted";
614
+ if (live && !ownerProvablyGone(existing)) {
615
+ return { kind: "joined", record: existing };
647
616
  }
648
- done({ ok: false, exitCode: null, reason: "Get-Process timed out" });
649
- }, WIN32_PROBE_TIMEOUT_MS);
650
- child.on(
651
- "error",
652
- (error) => done({
653
- ok: false,
654
- exitCode: null,
655
- reason: `PowerShell failed (${String(error.code ?? "unknown")})`
656
- })
657
- );
658
- child.on("close", (code) => {
659
- if (code === 0) done({ ok: true, stdout });
660
- else done({ ok: false, exitCode: code, reason: `Get-Process exited ${code}` });
617
+ }
618
+ const record = {
619
+ attemptId: `att_${randomUUID2()}`,
620
+ ownerPid: process.pid,
621
+ ownerStartedAt: processStartedAt(),
622
+ harnessId: key.harnessId,
623
+ target: key.target,
624
+ packVersion: key.packVersion,
625
+ treeDigest: key.treeDigest,
626
+ state: "reserved",
627
+ startedAt: Date.now(),
628
+ updatedAt: Date.now()
629
+ };
630
+ await writeJsonAtomic(file, record);
631
+ return { kind: "started", record };
632
+ });
633
+ }
634
+ async function updateInstallAttempt(key, attemptId, patch) {
635
+ const file = join2(installRootOf(key), OPERATION_FILE);
636
+ return lockFor(key)(async () => {
637
+ const existing = await readJson(file);
638
+ if (existing === null || existing.attemptId !== attemptId) return false;
639
+ await writeJsonAtomic(file, {
640
+ ...existing,
641
+ ...patch,
642
+ updatedAt: Date.now()
661
643
  });
644
+ return true;
662
645
  });
663
- if (!result.ok) {
664
- if (result.exitCode === 1) return { state: "gone" };
665
- return { state: "unknown", reason: result.reason };
666
- }
667
- const parsed = parseWin32ProbeLine(result.stdout);
668
- if (parsed === null) {
669
- return { state: "unknown", reason: "unparseable Get-Process output" };
670
- }
646
+ }
647
+ async function attemptStillOwns(key, attemptId) {
648
+ const record = await readJson(
649
+ join2(installRootOf(key), OPERATION_FILE)
650
+ );
651
+ return record?.attemptId === attemptId;
652
+ }
653
+ async function reserveRuntimeUse(args) {
654
+ const dir = join2(installRootOf(args.key), USE_DIR);
655
+ await mkdir2(dir, { recursive: true, mode: 448 });
656
+ const file = join2(dir, `${process.pid}-${randomUUID2()}.json`);
657
+ const record = {
658
+ pid: process.pid,
659
+ startedAt: processStartedAt(),
660
+ runtimeRoot: args.runtimeRoot,
661
+ at: Date.now(),
662
+ ...args.label ? { label: args.label } : {}
663
+ };
664
+ await writeJsonAtomic(file, record);
665
+ let released = false;
671
666
  return {
672
- state: "alive",
673
- identity: `win32:${parsed.fileTime}|${parsed.name}`
667
+ release: async () => {
668
+ if (released) return;
669
+ released = true;
670
+ await rm2(file, { force: true }).catch(() => {
671
+ });
672
+ }
674
673
  };
675
674
  }
676
- async function probeProcess(pid, platform = process.platform) {
677
- if (!Number.isInteger(pid) || pid <= 0) {
678
- return { state: "unknown", reason: "implausible pid" };
675
+ async function runtimeUseState(args) {
676
+ const dir = join2(installRootOf(args.key), USE_DIR);
677
+ let entries;
678
+ try {
679
+ entries = await readdir(dir);
680
+ } catch {
681
+ return { busy: false, holders: [] };
679
682
  }
680
- if (platform === "linux") return probeLinux(pid);
681
- if (platform === "darwin") return probeDarwin(pid);
682
- if (platform === "win32") return probeWin32(pid);
683
- return { state: "unknown", reason: `no liveness primitive on ${platform}` };
684
- }
685
- async function readProcessBirthIdentity(pid, platform = process.platform) {
686
- const probe = await probeProcess(pid, platform);
687
- return probe.state === "alive" ? probe.identity : null;
688
- }
689
- function supportsOwnershipProof(platform = process.platform) {
690
- if (platform === "linux" || platform === "darwin") return true;
691
- if (platform === "win32") return windowsJobLauncherVerified;
692
- return false;
693
- }
694
- var windowsJobLauncherVerified = false;
695
- function setWindowsJobLauncherVerified(verified) {
696
- windowsJobLauncherVerified = verified;
697
- }
698
- function sameBirthIdentity(recorded, current) {
699
- if (recorded === current) return true;
700
- const r = /^darwin:([^|]+)\|([\s\S]*)$/.exec(recorded);
701
- const c = /^darwin:([^|]+)\|([\s\S]*)$/.exec(current);
702
- if (r === null || c === null) return false;
703
- return r[1] === c[1] && /^\([^()]+\)$/.test(c[2]);
704
- }
705
- async function listGroupMembers(pgid, platform = process.platform) {
706
- let pids = [];
707
- if (platform === "darwin") {
708
- const out = await new Promise((resolve) => {
709
- execFile(
710
- "/bin/ps",
711
- ["-o", "pid=,state=", "-g", String(pgid)],
712
- { timeout: PS_TIMEOUT_MS, maxBuffer: 64 * 1024, encoding: "utf8", env: {} },
713
- (error, stdout) => resolve(error ? error.code === 1 ? "" : null : String(stdout))
714
- );
715
- });
716
- if (out === null) return null;
717
- pids = out.split("\n").map((l) => l.trim().split(/\s+/)).filter((f) => f.length >= 2 && /^\d+$/.test(f[0]) && !isDeadState(f[1].charAt(0))).map((f) => Number(f[0]));
718
- } else if (platform === "linux") {
719
- let entries;
720
- try {
721
- entries = await readdir("/proc");
722
- } catch {
723
- return null;
724
- }
725
- for (const entry of entries) {
726
- if (!/^\d+$/.test(entry)) continue;
727
- let raw;
728
- try {
729
- raw = await readFile(`/proc/${entry}/stat`, "utf8");
730
- } catch {
731
- continue;
732
- }
733
- const parsed = parseProcStatGroup(raw);
734
- if (parsed !== null && parsed.pgrp === pgid && !isDeadState(parsed.state.charAt(0))) pids.push(Number(entry));
735
- }
736
- } else {
737
- return null;
738
- }
739
- const members = [];
740
- for (const pid of pids) {
741
- if (pid === pgid) continue;
742
- const identity = await readProcessBirthIdentity(pid, platform);
743
- if (identity !== null) members.push({ pid, identity });
744
- }
745
- return members;
746
- }
747
- async function terminateOwnedProcess(args) {
748
- const platform = args.platform ?? process.platform;
749
- const initial = await probeProcess(args.pid, platform);
750
- if (initial.state === "gone") return "already-gone";
751
- if (initial.state === "unknown") return "unknown";
752
- if (!sameBirthIdentity(args.identity, initial.identity)) return "not-owned";
753
- const waitGone = async (ms) => {
754
- const until = Date.now() + ms;
755
- while (Date.now() < until) {
756
- const p = await probeProcess(args.pid, platform);
757
- if (p.state === "gone") return true;
758
- await new Promise((r) => setTimeout(r, 50));
683
+ const holders = [];
684
+ for (const entry of entries) {
685
+ if (!entry.endsWith(".json")) continue;
686
+ const file = join2(dir, entry);
687
+ const record = await readJson(file);
688
+ if (record === null) {
689
+ continue;
759
690
  }
760
- return false;
761
- };
762
- try {
763
- process.kill(args.pid, "SIGTERM");
764
- } catch {
765
- return "already-gone";
766
- }
767
- if (await waitGone(args.graceMs)) return "graceful";
768
- const again = await probeProcess(args.pid, platform);
769
- if (again.state === "gone") return "graceful";
770
- if (again.state === "unknown") return "unknown";
771
- if (!sameBirthIdentity(args.identity, again.identity)) return "not-owned";
772
- try {
773
- process.kill(args.pid, "SIGKILL");
774
- } catch {
775
- return "graceful";
776
- }
777
- return await waitGone(500) ? "forced" : "escaped";
778
- }
779
- async function probeProcessGroup(pid, platform = process.platform) {
780
- if (!Number.isInteger(pid) || pid <= 0) return "unknown";
781
- if (platform === "linux") return probeLinuxGroup(pid);
782
- if (platform === "darwin") return probeDarwinGroup(pid);
783
- if (platform === "win32") {
784
- const root = await probeProcess(pid, platform);
785
- if (root.state === "gone") return "empty";
786
- if (root.state === "alive") return "live";
787
- return "unknown";
788
- }
789
- return "unknown";
790
- }
791
- async function readProcessGroupId(pid, platform = process.platform) {
792
- if (!Number.isInteger(pid) || pid <= 0) return null;
793
- if (platform === "linux") {
794
- try {
795
- const raw = await readFile(`/proc/${pid}/stat`, "utf8");
796
- return parseProcStatGroup(raw)?.pgrp ?? null;
797
- } catch {
798
- return null;
691
+ if (record.runtimeRoot !== args.runtimeRoot) continue;
692
+ if (ownerProvablyGone({ ownerPid: record.pid, ownerStartedAt: record.startedAt })) {
693
+ await rm2(file, { force: true }).catch(() => {
694
+ });
695
+ continue;
799
696
  }
697
+ holders.push(record.pid);
800
698
  }
801
- if (platform === "darwin") {
802
- return new Promise((resolvePromise) => {
803
- execFile(
804
- "/bin/ps",
805
- ["-o", "pgid=", "-p", String(pid)],
806
- {
807
- timeout: PS_TIMEOUT_MS,
808
- maxBuffer: 4 * 1024,
809
- encoding: "utf8",
810
- env: {}
811
- },
812
- (error, stdout) => {
813
- if (error) {
814
- resolvePromise(null);
815
- return;
816
- }
817
- const parsed = Number(String(stdout).trim());
818
- resolvePromise(Number.isInteger(parsed) && parsed > 0 ? parsed : null);
819
- }
820
- );
821
- });
822
- }
823
- return null;
699
+ return { busy: holders.length > 0, holders };
824
700
  }
825
- function parseProcStatGroup(raw) {
826
- const close = raw.lastIndexOf(")");
827
- if (close === -1) return null;
828
- const fields = raw.slice(close + 1).trim().split(/\s+/);
829
- const state = fields[0];
830
- const pgrp = Number(fields[2]);
831
- if (state === void 0 || !Number.isInteger(pgrp)) return null;
832
- return { state, pgrp };
701
+ async function claimStagingDirectory(args) {
702
+ await writeJsonAtomic(join2(args.staging, STAGING_OWNER_FILE), {
703
+ pid: process.pid,
704
+ startedAt: processStartedAt(),
705
+ attemptId: args.attemptId,
706
+ at: Date.now()
707
+ });
833
708
  }
834
- async function probeLinuxGroup(pgid) {
709
+ async function sweepAbandonedStaging(key) {
710
+ const root = installRootOf(key);
835
711
  let entries;
836
712
  try {
837
- entries = await readdir("/proc");
713
+ entries = await readdir(root);
838
714
  } catch {
839
- return "unknown";
715
+ return { removed: 0, left: 0 };
840
716
  }
841
- let blind = false;
717
+ let removed = 0;
718
+ let left = 0;
842
719
  for (const entry of entries) {
843
- if (!/^\d+$/.test(entry)) continue;
844
- let raw;
845
- try {
846
- raw = await readFile(`/proc/${entry}/stat`, "utf8");
847
- } catch (error) {
848
- const code = error.code;
849
- if (code !== "ENOENT" && code !== "ESRCH") blind = true;
720
+ if (!entry.startsWith(STAGING_PREFIX)) continue;
721
+ const path = join2(root, entry);
722
+ const owner = await readJson(join2(path, STAGING_OWNER_FILE));
723
+ if (owner === null) {
724
+ left += 1;
850
725
  continue;
851
726
  }
852
- const parsed = parseProcStatGroup(raw);
853
- if (parsed === null) {
854
- blind = true;
727
+ if (!ownerProvablyGone({ ownerPid: owner.pid, ownerStartedAt: owner.startedAt })) {
728
+ left += 1;
855
729
  continue;
856
730
  }
857
- if (parsed.pgrp !== pgid) continue;
858
- if (!isDeadState(parsed.state.charAt(0))) return "live";
731
+ await rm2(path, { recursive: true, force: true }).catch(() => {
732
+ });
733
+ removed += 1;
859
734
  }
860
- return blind ? "unknown" : "empty";
735
+ if (removed > 0 || left > 0) {
736
+ logger.debug("[local-harness] staging sweep", { removed, left });
737
+ }
738
+ return { removed, left };
861
739
  }
862
- async function probeDarwinGroup(pgid) {
863
- const result = await new Promise((resolve) => {
864
- execFile(
865
- "/bin/ps",
866
- ["-o", "state=", "-g", String(pgid)],
867
- {
868
- timeout: PS_TIMEOUT_MS,
869
- maxBuffer: 64 * 1024,
870
- encoding: "utf8",
871
- env: {}
872
- },
873
- (error, out) => {
874
- if (!error) {
875
- resolve({ ok: true, stdout: typeof out === "string" ? out : "" });
876
- return;
877
- }
878
- const err = error;
879
- resolve({ ok: false, empty: err.killed !== true && err.code === 1 });
880
- }
740
+ async function recoverInterruptedActivation(args) {
741
+ const previous = `${args.versionRoot}${PREVIOUS_SUFFIX}`;
742
+ const hasPrevious = await stat2(previous).then(
743
+ (info) => info.isDirectory(),
744
+ () => false
745
+ );
746
+ if (!hasPrevious) return false;
747
+ return lockFor(args.key)(async () => {
748
+ const current = await stat2(args.versionRoot).then(
749
+ (info) => info.isDirectory(),
750
+ () => false
881
751
  );
882
- });
883
- if (!result.ok) return result.empty ? "empty" : "unknown";
884
- const states = result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
885
- if (states.some((state) => !isDeadState(state.charAt(0)))) return "live";
886
- return "empty";
887
- }
888
- function signalProcessGroup(pid, signal, platform = process.platform) {
889
- if (!Number.isInteger(pid) || pid <= 0) return false;
890
- try {
891
- if (platform === "win32") {
892
- process.kill(pid, signal);
893
- return true;
752
+ if (current) {
753
+ await rm2(previous, { recursive: true, force: true }).catch(() => {
754
+ });
755
+ return false;
894
756
  }
895
- process.kill(-pid, signal);
757
+ await rename(previous, args.versionRoot);
758
+ logger.warn("[local-harness] restored a runtime after an interrupted activation", {
759
+ packVersion: args.key.packVersion
760
+ });
896
761
  return true;
897
- } catch (error) {
898
- if (error.code === "ESRCH") return true;
899
- return false;
900
- }
762
+ });
901
763
  }
902
- async function terminateOwnedProcessGroup(args) {
903
- const platform = args.platform ?? process.platform;
904
- const pollMs = args.pollMs ?? 50;
905
- const settleGroup = async (goneOutcome, anchored) => {
906
- const probeGroup = args.probeGroup ?? probeProcessGroup;
907
- const before = await probeGroup(args.pid, platform);
908
- if (before === "empty") return { outcome: goneOutcome };
909
- if (before === "unknown") {
910
- return {
911
- outcome: "unknown",
912
- reason: "the process group could not be enumerated, so a survivor could be neither ruled out nor signalled without risking a reused group id"
913
- };
914
- }
915
- if (!anchored) {
764
+ var PREVIOUS_SUFFIX = ".mcpjam-previous";
765
+ function withRuntimeLifecycleLock(key, operation) {
766
+ return lockFor(key)(operation);
767
+ }
768
+
769
+ // server/utils/harness/local/runtime-install.ts
770
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
771
+ import { createReadStream, createWriteStream } from "fs";
772
+ import {
773
+ mkdir as mkdir3,
774
+ readdir as readdir4,
775
+ readFile as readFile5,
776
+ rename as rename2,
777
+ rm as rm3,
778
+ stat as stat4,
779
+ writeFile as writeFile3
780
+ } from "fs/promises";
781
+ import { homedir } from "os";
782
+ import { basename, join as join5 } from "path";
783
+ import { pipeline } from "stream/promises";
784
+ import { Readable } from "stream";
785
+ import { x as extractTar } from "tar";
786
+
787
+ // server/utils/harness/local/pack-digests.generated.ts
788
+ var PACK_TREE_DIGESTS = {
789
+ "claude-code": {},
790
+ codex: {}
791
+ };
792
+ var PACK_RECORDS = {
793
+ "claude-code": {},
794
+ codex: {}
795
+ };
796
+ var EXPECTED_PACK_VERSION = "";
797
+
798
+ // server/utils/harness/local/pack-signing-key.ts
799
+ import { createPublicKey, verify as edVerify } from "crypto";
800
+ var PACK_SIGNING_KEYS = [
801
+ {
802
+ // Rotated before the first pack publication on 2026-09-28. The private
803
+ // key was uploaded directly to the protected release environment; no
804
+ // file or repository-level secret retains it. Recovery is rotation.
805
+ keyId: "pack-2026-09-28",
806
+ publicKeyPem: `-----BEGIN PUBLIC KEY-----
807
+ MCowBQYDK2VwAyEA/8fkvTTgyzjmmlLmN4veODTVpexBi4Ib6SGVq8xmyTs=
808
+ -----END PUBLIC KEY-----
809
+ `
810
+ }
811
+ ];
812
+ function verifyPackManifestSignature(manifestBytes, signatureBase64, keys = PACK_SIGNING_KEYS) {
813
+ if (keys.length === 0) {
814
+ return {
815
+ ok: false,
816
+ reason: "no-keys",
817
+ message: "this Inspector build carries no runtime pack signing key, so it cannot verify that a downloaded pack came from MCPJam. Packs are installable from a local path for development only."
818
+ };
819
+ }
820
+ let signature;
821
+ try {
822
+ signature = Buffer.from(signatureBase64.trim(), "base64");
823
+ if (signature.length === 0) throw new Error("empty");
824
+ } catch {
825
+ return {
826
+ ok: false,
827
+ reason: "malformed",
828
+ message: "the pack manifest signature is not valid base64"
829
+ };
830
+ }
831
+ for (const key of keys) {
832
+ try {
833
+ const publicKey = createPublicKey(key.publicKeyPem);
834
+ if (edVerify(null, manifestBytes, publicKey, signature)) {
835
+ return { ok: true, keyId: key.keyId };
836
+ }
837
+ } catch {
838
+ continue;
839
+ }
840
+ }
841
+ return {
842
+ ok: false,
843
+ reason: "bad-signature",
844
+ message: "the runtime pack manifest is not signed by any key this Inspector trusts, so the pack is refused"
845
+ };
846
+ }
847
+
848
+ // server/utils/harness/local/runtime-identity.ts
849
+ import { createHash } from "crypto";
850
+ import { execFile as execFile2 } from "child_process";
851
+ import { readdir as readdir3, readFile as readFile4, realpath, stat as stat3 } from "fs/promises";
852
+ import { dirname, isAbsolute, join as join4, normalize, relative, sep } from "path";
853
+
854
+ // server/utils/harness/local/process-identity.ts
855
+ import { execFile, spawn } from "child_process";
856
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
857
+ import { join as join3 } from "path";
858
+ var PS_TIMEOUT_MS = 3e3;
859
+ function parseLinuxProcStat(raw) {
860
+ const close = raw.lastIndexOf(")");
861
+ if (close === -1) return null;
862
+ const fields = raw.slice(close + 2).split(" ");
863
+ const state = fields[0];
864
+ const starttime = fields[19];
865
+ if (!state || !/^[A-Za-z]$/.test(state)) return null;
866
+ if (!starttime || !/^\d+$/.test(starttime)) return null;
867
+ return { state, starttime };
868
+ }
869
+ function isDeadState(state) {
870
+ return state === "Z" || state === "X" || state === "x";
871
+ }
872
+ async function probeLinux(pid) {
873
+ let raw;
874
+ try {
875
+ raw = await readFile3(`/proc/${pid}/stat`, "utf8");
876
+ } catch (error) {
877
+ const code = error.code;
878
+ if (code === "ENOENT" || code === "ESRCH") return { state: "gone" };
879
+ return {
880
+ state: "unknown",
881
+ reason: `/proc read failed (${code ?? "unknown"})`
882
+ };
883
+ }
884
+ const parsed = parseLinuxProcStat(raw);
885
+ if (parsed === null) {
886
+ return { state: "unknown", reason: "unparseable /proc stat line" };
887
+ }
888
+ if (isDeadState(parsed.state)) return { state: "gone" };
889
+ return { state: "alive", identity: `linux:${parsed.starttime}` };
890
+ }
891
+ function parseDarwinPsLine(raw) {
892
+ const line = raw.trim();
893
+ if (line.length === 0) return null;
894
+ const match = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\s\S]+)$/.exec(line);
895
+ if (match === null) return null;
896
+ const state = match[1];
897
+ const lstart = [match[2], match[3], match[4], match[5], match[6]].join(" ");
898
+ const command = match[7];
899
+ return { state, lstart, command };
900
+ }
901
+ async function probeDarwin(pid) {
902
+ const result = await new Promise((resolve) => {
903
+ execFile(
904
+ "/bin/ps",
905
+ ["-o", "state=,lstart=,command=", "-p", String(pid)],
906
+ {
907
+ timeout: PS_TIMEOUT_MS,
908
+ maxBuffer: 16 * 1024,
909
+ encoding: "utf8",
910
+ env: {}
911
+ },
912
+ (error, out) => {
913
+ if (!error) {
914
+ resolve({ ok: true, stdout: typeof out === "string" ? out : "" });
915
+ return;
916
+ }
917
+ const err = error;
918
+ resolve({
919
+ ok: false,
920
+ // `ps` exits 1 when no process matches — that IS an answer. A kill
921
+ // (timeout) or a spawn failure is not.
922
+ exitCode: typeof err.code === "number" ? err.code : null,
923
+ reason: err.killed ? "ps timed out" : `ps failed (${String(err.code ?? "unknown")})`
924
+ });
925
+ }
926
+ );
927
+ });
928
+ if (!result.ok) {
929
+ if (result.exitCode === 1) return { state: "gone" };
930
+ return { state: "unknown", reason: result.reason };
931
+ }
932
+ const parsed = parseDarwinPsLine(result.stdout);
933
+ if (parsed === null) {
934
+ return result.stdout.trim().length === 0 ? { state: "gone" } : { state: "unknown", reason: "unparseable ps output" };
935
+ }
936
+ if (isDeadState(parsed.state.charAt(0))) return { state: "gone" };
937
+ return {
938
+ state: "alive",
939
+ identity: `darwin:${parsed.lstart}|${parsed.command}`
940
+ };
941
+ }
942
+ var WIN32_PROBE_TIMEOUT_MS = 15e3;
943
+ function win32SystemRoot() {
944
+ return process.env.SYSTEMROOT ?? process.env.WINDIR ?? "C:\\Windows";
945
+ }
946
+ function win32PowerShellPath() {
947
+ return join3(
948
+ win32SystemRoot(),
949
+ "System32",
950
+ "WindowsPowerShell",
951
+ "v1.0",
952
+ "powershell.exe"
953
+ );
954
+ }
955
+ function parseWin32ProbeLine(raw) {
956
+ const line = raw.trim();
957
+ const match = /^(\d+)\|(.+)$/.exec(line);
958
+ if (match === null) return null;
959
+ return { fileTime: match[1], name: match[2] };
960
+ }
961
+ async function probeWin32(pid) {
962
+ try {
963
+ process.kill(pid, 0);
964
+ } catch (error) {
965
+ const code = error.code;
966
+ if (code === "ESRCH") return { state: "gone" };
967
+ if (code !== "EPERM") {
968
+ return {
969
+ state: "unknown",
970
+ reason: `kill(0) failed (${code ?? "unknown"})`
971
+ };
972
+ }
973
+ }
974
+ const script = [
975
+ `$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue`,
976
+ "if ($null -eq $p) { exit 1 }",
977
+ "try { $t = $p.StartTime.ToFileTimeUtc() } catch { exit 3 }",
978
+ "Write-Output ('{0}|{1}' -f $t, $p.ProcessName)"
979
+ ].join("; ");
980
+ const result = await new Promise((resolve) => {
981
+ let settled = false;
982
+ let timer;
983
+ const done = (value) => {
984
+ if (settled) return;
985
+ settled = true;
986
+ if (timer !== void 0) clearTimeout(timer);
987
+ resolve(value);
988
+ };
989
+ let child;
990
+ try {
991
+ child = spawn(
992
+ win32PowerShellPath(),
993
+ [
994
+ "-NoProfile",
995
+ "-NonInteractive",
996
+ "-InputFormat",
997
+ "None",
998
+ "-ExecutionPolicy",
999
+ "Bypass",
1000
+ "-Command",
1001
+ script
1002
+ ],
1003
+ { stdio: ["ignore", "pipe", "pipe"], env: process.env, windowsHide: true }
1004
+ );
1005
+ } catch (error) {
1006
+ done({
1007
+ ok: false,
1008
+ exitCode: null,
1009
+ reason: `PowerShell failed to start (${String(
1010
+ error.code ?? "unknown"
1011
+ )})`
1012
+ });
1013
+ return;
1014
+ }
1015
+ let stdout = "";
1016
+ child.stdout?.setEncoding("utf8");
1017
+ child.stdout?.on("data", (chunk) => {
1018
+ if (stdout.length < 16 * 1024) stdout += chunk;
1019
+ });
1020
+ child.stderr?.resume();
1021
+ timer = setTimeout(() => {
1022
+ try {
1023
+ child.kill();
1024
+ } catch {
1025
+ }
1026
+ done({ ok: false, exitCode: null, reason: "Get-Process timed out" });
1027
+ }, WIN32_PROBE_TIMEOUT_MS);
1028
+ child.on(
1029
+ "error",
1030
+ (error) => done({
1031
+ ok: false,
1032
+ exitCode: null,
1033
+ reason: `PowerShell failed (${String(error.code ?? "unknown")})`
1034
+ })
1035
+ );
1036
+ child.on("close", (code) => {
1037
+ if (code === 0) done({ ok: true, stdout });
1038
+ else done({ ok: false, exitCode: code, reason: `Get-Process exited ${code}` });
1039
+ });
1040
+ });
1041
+ if (!result.ok) {
1042
+ if (result.exitCode === 1) return { state: "gone" };
1043
+ return { state: "unknown", reason: result.reason };
1044
+ }
1045
+ const parsed = parseWin32ProbeLine(result.stdout);
1046
+ if (parsed === null) {
1047
+ return { state: "unknown", reason: "unparseable Get-Process output" };
1048
+ }
1049
+ return {
1050
+ state: "alive",
1051
+ identity: `win32:${parsed.fileTime}|${parsed.name}`
1052
+ };
1053
+ }
1054
+ async function probeProcess(pid, platform = process.platform) {
1055
+ if (!Number.isInteger(pid) || pid <= 0) {
1056
+ return { state: "unknown", reason: "implausible pid" };
1057
+ }
1058
+ if (platform === "linux") return probeLinux(pid);
1059
+ if (platform === "darwin") return probeDarwin(pid);
1060
+ if (platform === "win32") return probeWin32(pid);
1061
+ return { state: "unknown", reason: `no liveness primitive on ${platform}` };
1062
+ }
1063
+ async function readProcessBirthIdentity(pid, platform = process.platform) {
1064
+ const probe = await probeProcess(pid, platform);
1065
+ return probe.state === "alive" ? probe.identity : null;
1066
+ }
1067
+ function supportsOwnershipProof(platform = process.platform) {
1068
+ if (platform === "linux" || platform === "darwin") return true;
1069
+ if (platform === "win32") return windowsJobLauncherVerified;
1070
+ return false;
1071
+ }
1072
+ var windowsJobLauncherVerified = false;
1073
+ function setWindowsJobLauncherVerified(verified) {
1074
+ windowsJobLauncherVerified = verified;
1075
+ }
1076
+ function sameBirthIdentity(recorded, current) {
1077
+ if (recorded === current) return true;
1078
+ const r = /^darwin:([^|]+)\|([\s\S]*)$/.exec(recorded);
1079
+ const c = /^darwin:([^|]+)\|([\s\S]*)$/.exec(current);
1080
+ if (r === null || c === null) return false;
1081
+ return r[1] === c[1] && /^\([^()]+\)$/.test(c[2]);
1082
+ }
1083
+ async function listGroupMembers(pgid, platform = process.platform) {
1084
+ let pids = [];
1085
+ if (platform === "darwin") {
1086
+ const out = await new Promise((resolve) => {
1087
+ execFile(
1088
+ "/bin/ps",
1089
+ ["-o", "pid=,state=", "-g", String(pgid)],
1090
+ { timeout: PS_TIMEOUT_MS, maxBuffer: 64 * 1024, encoding: "utf8", env: {} },
1091
+ (error, stdout) => resolve(error ? error.code === 1 ? "" : null : String(stdout))
1092
+ );
1093
+ });
1094
+ if (out === null) return null;
1095
+ pids = out.split("\n").map((l) => l.trim().split(/\s+/)).filter((f) => f.length >= 2 && /^\d+$/.test(f[0]) && !isDeadState(f[1].charAt(0))).map((f) => Number(f[0]));
1096
+ } else if (platform === "linux") {
1097
+ let entries;
1098
+ try {
1099
+ entries = await readdir2("/proc");
1100
+ } catch {
1101
+ return null;
1102
+ }
1103
+ for (const entry of entries) {
1104
+ if (!/^\d+$/.test(entry)) continue;
1105
+ let raw;
1106
+ try {
1107
+ raw = await readFile3(`/proc/${entry}/stat`, "utf8");
1108
+ } catch {
1109
+ continue;
1110
+ }
1111
+ const parsed = parseProcStatGroup(raw);
1112
+ if (parsed !== null && parsed.pgrp === pgid && !isDeadState(parsed.state.charAt(0))) pids.push(Number(entry));
1113
+ }
1114
+ } else {
1115
+ return null;
1116
+ }
1117
+ const members = [];
1118
+ for (const pid of pids) {
1119
+ if (pid === pgid) continue;
1120
+ const identity = await readProcessBirthIdentity(pid, platform);
1121
+ if (identity !== null) members.push({ pid, identity });
1122
+ }
1123
+ return members;
1124
+ }
1125
+ async function terminateOwnedProcess(args) {
1126
+ const platform = args.platform ?? process.platform;
1127
+ const initial = await probeProcess(args.pid, platform);
1128
+ if (initial.state === "gone") return "already-gone";
1129
+ if (initial.state === "unknown") return "unknown";
1130
+ if (!sameBirthIdentity(args.identity, initial.identity)) return "not-owned";
1131
+ const waitGone = async (ms) => {
1132
+ const until = Date.now() + ms;
1133
+ while (Date.now() < until) {
1134
+ const p = await probeProcess(args.pid, platform);
1135
+ if (p.state === "gone") return true;
1136
+ await new Promise((r) => setTimeout(r, 50));
1137
+ }
1138
+ return false;
1139
+ };
1140
+ try {
1141
+ process.kill(args.pid, "SIGTERM");
1142
+ } catch {
1143
+ return "already-gone";
1144
+ }
1145
+ if (await waitGone(args.graceMs)) return "graceful";
1146
+ const again = await probeProcess(args.pid, platform);
1147
+ if (again.state === "gone") return "graceful";
1148
+ if (again.state === "unknown") return "unknown";
1149
+ if (!sameBirthIdentity(args.identity, again.identity)) return "not-owned";
1150
+ try {
1151
+ process.kill(args.pid, "SIGKILL");
1152
+ } catch {
1153
+ return "graceful";
1154
+ }
1155
+ return await waitGone(500) ? "forced" : "escaped";
1156
+ }
1157
+ async function probeProcessGroup(pid, platform = process.platform) {
1158
+ if (!Number.isInteger(pid) || pid <= 0) return "unknown";
1159
+ if (platform === "linux") return probeLinuxGroup(pid);
1160
+ if (platform === "darwin") return probeDarwinGroup(pid);
1161
+ if (platform === "win32") {
1162
+ const root = await probeProcess(pid, platform);
1163
+ if (root.state === "gone") return "empty";
1164
+ if (root.state === "alive") return "live";
1165
+ return "unknown";
1166
+ }
1167
+ return "unknown";
1168
+ }
1169
+ async function readProcessGroupId(pid, platform = process.platform) {
1170
+ if (!Number.isInteger(pid) || pid <= 0) return null;
1171
+ if (platform === "linux") {
1172
+ try {
1173
+ const raw = await readFile3(`/proc/${pid}/stat`, "utf8");
1174
+ return parseProcStatGroup(raw)?.pgrp ?? null;
1175
+ } catch {
1176
+ return null;
1177
+ }
1178
+ }
1179
+ if (platform === "darwin") {
1180
+ return new Promise((resolvePromise) => {
1181
+ execFile(
1182
+ "/bin/ps",
1183
+ ["-o", "pgid=", "-p", String(pid)],
1184
+ {
1185
+ timeout: PS_TIMEOUT_MS,
1186
+ maxBuffer: 4 * 1024,
1187
+ encoding: "utf8",
1188
+ env: {}
1189
+ },
1190
+ (error, stdout) => {
1191
+ if (error) {
1192
+ resolvePromise(null);
1193
+ return;
1194
+ }
1195
+ const parsed = Number(String(stdout).trim());
1196
+ resolvePromise(Number.isInteger(parsed) && parsed > 0 ? parsed : null);
1197
+ }
1198
+ );
1199
+ });
1200
+ }
1201
+ return null;
1202
+ }
1203
+ function parseProcStatGroup(raw) {
1204
+ const close = raw.lastIndexOf(")");
1205
+ if (close === -1) return null;
1206
+ const fields = raw.slice(close + 1).trim().split(/\s+/);
1207
+ const state = fields[0];
1208
+ const pgrp = Number(fields[2]);
1209
+ if (state === void 0 || !Number.isInteger(pgrp)) return null;
1210
+ return { state, pgrp };
1211
+ }
1212
+ async function probeLinuxGroup(pgid) {
1213
+ let entries;
1214
+ try {
1215
+ entries = await readdir2("/proc");
1216
+ } catch {
1217
+ return "unknown";
1218
+ }
1219
+ let blind = false;
1220
+ for (const entry of entries) {
1221
+ if (!/^\d+$/.test(entry)) continue;
1222
+ let raw;
1223
+ try {
1224
+ raw = await readFile3(`/proc/${entry}/stat`, "utf8");
1225
+ } catch (error) {
1226
+ const code = error.code;
1227
+ if (code !== "ENOENT" && code !== "ESRCH") blind = true;
1228
+ continue;
1229
+ }
1230
+ const parsed = parseProcStatGroup(raw);
1231
+ if (parsed === null) {
1232
+ blind = true;
1233
+ continue;
1234
+ }
1235
+ if (parsed.pgrp !== pgid) continue;
1236
+ if (!isDeadState(parsed.state.charAt(0))) return "live";
1237
+ }
1238
+ return blind ? "unknown" : "empty";
1239
+ }
1240
+ async function probeDarwinGroup(pgid) {
1241
+ const result = await new Promise((resolve) => {
1242
+ execFile(
1243
+ "/bin/ps",
1244
+ ["-o", "state=", "-g", String(pgid)],
1245
+ {
1246
+ timeout: PS_TIMEOUT_MS,
1247
+ maxBuffer: 64 * 1024,
1248
+ encoding: "utf8",
1249
+ env: {}
1250
+ },
1251
+ (error, out) => {
1252
+ if (!error) {
1253
+ resolve({ ok: true, stdout: typeof out === "string" ? out : "" });
1254
+ return;
1255
+ }
1256
+ const err = error;
1257
+ resolve({ ok: false, empty: err.killed !== true && err.code === 1 });
1258
+ }
1259
+ );
1260
+ });
1261
+ if (!result.ok) return result.empty ? "empty" : "unknown";
1262
+ const states = result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1263
+ if (states.some((state) => !isDeadState(state.charAt(0)))) return "live";
1264
+ return "empty";
1265
+ }
1266
+ function signalProcessGroup(pid, signal, platform = process.platform) {
1267
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1268
+ try {
1269
+ if (platform === "win32") {
1270
+ process.kill(pid, signal);
1271
+ return true;
1272
+ }
1273
+ process.kill(-pid, signal);
1274
+ return true;
1275
+ } catch (error) {
1276
+ if (error.code === "ESRCH") return true;
1277
+ return false;
1278
+ }
1279
+ }
1280
+ async function terminateOwnedProcessGroup(args) {
1281
+ const platform = args.platform ?? process.platform;
1282
+ const pollMs = args.pollMs ?? 50;
1283
+ const settleGroup = async (goneOutcome, anchored) => {
1284
+ const probeGroup = args.probeGroup ?? probeProcessGroup;
1285
+ const before = await probeGroup(args.pid, platform);
1286
+ if (before === "empty") return { outcome: goneOutcome };
1287
+ if (before === "unknown") {
1288
+ return {
1289
+ outcome: "unknown",
1290
+ reason: "the process group could not be enumerated, so a survivor could be neither ruled out nor signalled without risking a reused group id"
1291
+ };
1292
+ }
1293
+ if (!anchored) {
916
1294
  return {
917
1295
  outcome: "unknown",
918
1296
  reason: "a live process group carries this id, but the root was already gone when the stop began, so the group cannot be tied to this tree"
@@ -985,10 +1363,10 @@ async function digestTreeWithSnapshot(root) {
985
1363
  let files = 0;
986
1364
  let bytes = 0;
987
1365
  const walk = async (dir) => {
988
- const entries = await readdir2(dir, { withFileTypes: true });
1366
+ const entries = await readdir3(dir, { withFileTypes: true });
989
1367
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
990
1368
  for (const entry of entries) {
991
- const full = join2(dir, entry.name);
1369
+ const full = join4(dir, entry.name);
992
1370
  const rel = relative(root, full).split(sep).join("/");
993
1371
  if (entry.isSymbolicLink()) {
994
1372
  throw new Error(
@@ -1010,12 +1388,12 @@ async function digestTreeWithSnapshot(root) {
1010
1388
  "managed runtime bundle exceeds the file-count ceiling"
1011
1389
  );
1012
1390
  }
1013
- const info = await stat(full);
1391
+ const info = await stat3(full);
1014
1392
  bytes += info.size;
1015
1393
  if (bytes > MAX_BUNDLE_BYTES) {
1016
1394
  throw new Error("managed runtime bundle exceeds the size ceiling");
1017
1395
  }
1018
- const content = await readFile2(full);
1396
+ const content = await readFile4(full);
1019
1397
  const executable = (info.mode & 73) !== 0 ? "1" : "0";
1020
1398
  const contentDigest = createHash("sha256").update(content).digest();
1021
1399
  hash.update(`f\0${rel}\0${executable}\0${info.size}\0`);
@@ -1126,11 +1504,11 @@ async function resolveManagedBundle(args) {
1126
1504
  };
1127
1505
  }
1128
1506
  const policy = manifest.runtime;
1129
- const root = join2(runtimeRoot, policy.bundleName);
1507
+ const root = join4(runtimeRoot, policy.bundleName);
1130
1508
  let canonicalRoot;
1131
1509
  try {
1132
1510
  canonicalRoot = await realpath(root);
1133
- const info = await stat(canonicalRoot);
1511
+ const info = await stat3(canonicalRoot);
1134
1512
  if (!info.isDirectory()) throw new Error("not a directory");
1135
1513
  } catch {
1136
1514
  return {
@@ -1170,7 +1548,7 @@ async function resolveManagedBundle(args) {
1170
1548
  }
1171
1549
  const digest = verification.digest;
1172
1550
  const insideBundle = async (relativePath, label, requireExecutable) => {
1173
- const full = join2(canonicalRoot, relativePath);
1551
+ const full = join4(canonicalRoot, relativePath);
1174
1552
  if (!normalize(full).startsWith(canonicalRoot + sep)) {
1175
1553
  return {
1176
1554
  ok: false,
@@ -1178,7 +1556,7 @@ async function resolveManagedBundle(args) {
1178
1556
  };
1179
1557
  }
1180
1558
  try {
1181
- const info = await stat(full);
1559
+ const info = await stat3(full);
1182
1560
  if (!info.isFile()) throw new Error("not a file");
1183
1561
  if (requireExecutable && platform !== "win32" && (info.mode & 73) === 0) {
1184
1562
  throw new Error("not executable");
@@ -1309,7 +1687,7 @@ async function firstUntrustedAncestor(path, platform) {
1309
1687
  for (; ; ) {
1310
1688
  let info;
1311
1689
  try {
1312
- info = await stat(dir);
1690
+ info = await stat3(dir);
1313
1691
  } catch {
1314
1692
  return dir;
1315
1693
  }
@@ -1343,7 +1721,7 @@ async function resolveSystemInstall(opts) {
1343
1721
  const rejections = [];
1344
1722
  for (const dir of searchPaths) {
1345
1723
  for (const name of policy.executableNames) {
1346
- const candidate = join2(dir, name);
1724
+ const candidate = join4(dir, name);
1347
1725
  let canonical;
1348
1726
  try {
1349
1727
  canonical = await realpath(candidate);
@@ -1361,7 +1739,7 @@ async function resolveSystemInstall(opts) {
1361
1739
  }
1362
1740
  let info;
1363
1741
  try {
1364
- info = await stat(canonical);
1742
+ info = await stat3(canonical);
1365
1743
  } catch {
1366
1744
  continue;
1367
1745
  }
@@ -1431,7 +1809,7 @@ async function resolveSystemInstall(opts) {
1431
1809
  });
1432
1810
  continue;
1433
1811
  }
1434
- const content = await readFile2(canonical);
1812
+ const content = await readFile4(canonical);
1435
1813
  const digest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
1436
1814
  accepted.push({
1437
1815
  runtimeId: runtimeIdOf([
@@ -1481,9 +1859,9 @@ async function detectSnapshotDrift(snapshot) {
1481
1859
  const recorded = new Map(snapshot.entries.map((e) => [e.path, e]));
1482
1860
  let seen = 0;
1483
1861
  const walk = async (dir) => {
1484
- const entries = await readdir2(dir, { withFileTypes: true });
1862
+ const entries = await readdir3(dir, { withFileTypes: true });
1485
1863
  for (const entry of entries) {
1486
- const full = join2(dir, entry.name);
1864
+ const full = join4(dir, entry.name);
1487
1865
  const rel = relative(snapshot.root, full).split(sep).join("/");
1488
1866
  if (entry.isSymbolicLink()) return `a symlink appeared at ${rel}`;
1489
1867
  if (entry.isDirectory()) {
@@ -1495,12 +1873,12 @@ async function detectSnapshotDrift(snapshot) {
1495
1873
  const expected = recorded.get(rel);
1496
1874
  if (expected === void 0) return `an unexpected file appeared at ${rel}`;
1497
1875
  seen += 1;
1498
- const info = await stat(full);
1876
+ const info = await stat3(full);
1499
1877
  if (info.size !== expected.size || info.mtimeMs !== expected.mtimeMs || info.ctimeMs !== expected.ctimeMs || Number(info.ino) !== expected.ino || info.mode !== expected.mode) {
1500
1878
  return `${rel} was modified`;
1501
1879
  }
1502
1880
  if (isRehashedOnRevalidate(rel)) {
1503
- const content = await readFile2(full);
1881
+ const content = await readFile4(full);
1504
1882
  const fileDigest = createHash("sha256").update(content).digest("hex");
1505
1883
  if (fileDigest !== snapshot.executableDigests[rel]) {
1506
1884
  return `${rel} was rewritten in place`;
@@ -1547,7 +1925,7 @@ async function revalidateRuntime(runtime) {
1547
1925
  message: reverified.reason === "digest-mismatch" ? `the ${runtime.harnessId} runtime bundle changed after consent was granted (${drift}; expected ${runtime.digest}, found ${reverified.digest})` : `the ${runtime.harnessId} runtime could not be re-verified: ${reverified.message}`
1548
1926
  };
1549
1927
  }
1550
- const content = await readFile2(runtime.launcherPath);
1928
+ const content = await readFile4(runtime.launcherPath);
1551
1929
  const digest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
1552
1930
  if (digest !== runtime.digest) {
1553
1931
  return {
@@ -1593,10 +1971,8 @@ var LOCAL_HARNESS_MANIFEST = {
1593
1971
  supportsBuiltinToolApprovals: true,
1594
1972
  permissionProfileMapping: {
1595
1973
  "read-only": "allow-reads",
1596
- "workspace-edits": "allow-edits"
1597
- // `unrestricted` is deliberately absent: Claude Code CAN surface
1598
- // approvals, so there is no reason to offer a profile that switches them
1599
- // off on a host.
1974
+ "workspace-edits": "allow-edits",
1975
+ unrestricted: "allow-all"
1600
1976
  },
1601
1977
  // Native eligibility is per platform. macOS and Linux have POSIX process
1602
1978
  // groups, which is what whole-tree cleanup is built on there.
@@ -1654,477 +2030,99 @@ var LOCAL_HARNESS_MANIFEST = {
1654
2030
  lifecycleConformanceVersion: "",
1655
2031
  adapterBootstrapDir: ".harness-bootstrap/codex",
1656
2032
  adapterBootstrapFiles: ["package.json", "pnpm-lock.yaml", "bridge.mjs"],
1657
- bridgeBundleDigest: `sha256:${"0".repeat(64)}`
1658
- }
1659
- };
1660
- function localPermissionModeFor(harnessId, permissionProfile, targetKind) {
1661
- const manifests = LOCAL_HARNESS_MANIFEST;
1662
- const manifest = Object.prototype.hasOwnProperty.call(manifests, harnessId) ? manifests[harnessId] : void 0;
1663
- if (manifest === void 0) return null;
1664
- if (permissionProfile === "unrestricted" && targetKind === "local-native") {
1665
- return null;
1666
- }
1667
- return manifest.permissionProfileMapping[permissionProfile] ?? null;
1668
- }
1669
- function resolveLocalCompatibility(query, manifests = LOCAL_HARNESS_MANIFEST) {
1670
- const manifest = Object.prototype.hasOwnProperty.call(
1671
- manifests,
1672
- query.harnessId
1673
- ) ? manifests[query.harnessId] : void 0;
1674
- if (!manifest) {
1675
- return {
1676
- ok: false,
1677
- status: "harness-not-supported",
1678
- message: `${query.harnessId} has no reviewed local compatibility manifest. Run it hosted, or add a manifest entry with conformance evidence.`
1679
- };
1680
- }
1681
- if (query.installedAdapterVersion !== manifest.adapterVersion) {
1682
- return {
1683
- ok: false,
1684
- status: "adapter-version-mismatch",
1685
- 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.`
1686
- };
1687
- }
1688
- if (manifest.lifecycleConformanceVersion === "") {
1689
- return {
1690
- ok: false,
1691
- status: "conformance-missing",
1692
- 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.`
1693
- };
1694
- }
1695
- if (query.platform === null) {
1696
- return {
1697
- ok: false,
1698
- status: "platform-not-supported",
1699
- // NOT "run the harness hosted instead". A requested-local turn has Send
1700
- // disabled in this state (`localHarnessBlocksSend`), so nothing falls
1701
- // back — and on a local Inspector with no cloud data plane there is no
1702
- // hosted target to switch to either, which makes the instruction
1703
- // unactionable exactly where it is most likely to be read.
1704
- message: `local execution is not supported on this platform.`
1705
- };
1706
- }
1707
- if (query.targetKind === "local-native") {
1708
- if (!manifest.nativePlatforms.includes(query.platform)) {
1709
- 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(", ")}).`;
1710
- return {
1711
- ok: false,
1712
- status: "native-not-eligible",
1713
- message: reason
1714
- };
1715
- }
1716
- } else {
1717
- if (query.backend === void 0) {
1718
- return {
1719
- ok: false,
1720
- status: "backend-not-verified",
1721
- message: "an isolated target must name its isolation backend."
1722
- };
1723
- }
1724
- const verifiedHere = manifest.isolatedBackends[query.platform] ?? [];
1725
- if (!verifiedHere.includes(query.backend)) {
1726
- return {
1727
- ok: false,
1728
- status: "backend-not-verified",
1729
- 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.`
1730
- };
1731
- }
1732
- }
1733
- const permissionMode = manifest.permissionProfileMapping[query.permissionProfile];
1734
- if (permissionMode === void 0) {
1735
- const offered = Object.keys(manifest.permissionProfileMapping);
1736
- return {
1737
- ok: false,
1738
- status: "permission-profile-not-supported",
1739
- 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.`)
1740
- };
1741
- }
1742
- if (query.permissionProfile === "unrestricted" && query.targetKind === "local-native") {
1743
- return {
1744
- ok: false,
1745
- status: "permission-profile-not-supported",
1746
- 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.`
1747
- };
1748
- }
1749
- return { ok: true, manifest, permissionMode };
1750
- }
1751
-
1752
- // server/utils/harness/local/runtime-lifecycle.ts
1753
- import { randomUUID as randomUUID2 } from "crypto";
1754
- import {
1755
- mkdir as mkdir2,
1756
- readdir as readdir3,
1757
- readFile as readFile4,
1758
- rename,
1759
- rm as rm2,
1760
- stat as stat3,
1761
- writeFile as writeFile2
1762
- } from "fs/promises";
1763
- import { join as join4 } from "path";
1764
-
1765
- // server/utils/harness/local/local-state-lock.ts
1766
- import { randomUUID } from "crypto";
1767
- import { mkdir, readFile as readFile3, rm, stat as stat2, writeFile } from "fs/promises";
1768
- import { join as join3 } from "path";
1769
- var DEFAULT_STALE_MS = 3e4;
1770
- var DEFAULT_POLL_MS = 25;
1771
- var DEFAULT_TIMEOUT_MS = 45e3;
1772
- async function readLockBody(file) {
1773
- try {
1774
- const parsed = JSON.parse(await readFile3(file, "utf8"));
1775
- if (!parsed || typeof parsed !== "object") return null;
1776
- const body = parsed;
1777
- if (typeof body.pid !== "number" || !Number.isInteger(body.pid) || body.pid <= 0 || typeof body.nonce !== "string" || body.nonce.length === 0) {
1778
- return null;
1779
- }
1780
- return { pid: body.pid, nonce: body.nonce, at: Number(body.at) || 0 };
1781
- } catch {
1782
- return null;
1783
- }
1784
- }
1785
- function lockHolderProvablyGone(body) {
1786
- if (body === null || body.pid === process.pid) return false;
1787
- try {
1788
- process.kill(body.pid, 0);
1789
- return false;
1790
- } catch (error) {
1791
- return error.code === "ESRCH";
1792
- }
1793
- }
1794
- function createLocalStateMutationLock(options) {
1795
- let mutationChain = Promise.resolve();
1796
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
1797
- const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
1798
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1799
- const acquire = async () => {
1800
- const root = options.rootDir();
1801
- const file = join3(root, options.lockFileName);
1802
- await mkdir(root, { recursive: true, mode: 448 });
1803
- const nonce = randomUUID();
1804
- const deadline = Date.now() + timeoutMs;
1805
- for (; ; ) {
1806
- try {
1807
- await writeFile(
1808
- file,
1809
- JSON.stringify({ pid: process.pid, nonce, at: Date.now() }),
1810
- { flag: "wx", mode: 384 }
1811
- );
1812
- return async () => {
1813
- const held = await readLockBody(file);
1814
- if (held?.nonce !== nonce) {
1815
- logger.warn(
1816
- `[local-harness] ${options.resourceLabel} lock was taken over; not releasing`
1817
- );
1818
- return;
1819
- }
1820
- await rm(file, { force: true }).catch(() => {
1821
- });
1822
- };
1823
- } catch (error) {
1824
- if (error.code !== "EEXIST") throw error;
1825
- const body = await readLockBody(file);
1826
- const age = await stat2(file).then(
1827
- (info) => Date.now() - info.mtimeMs,
1828
- () => 0
1829
- );
1830
- const holderGone = lockHolderProvablyGone(body);
1831
- const unattributable = body === null && age > staleMs;
1832
- if (holderGone || unattributable) {
1833
- logger.warn(
1834
- `[local-harness] breaking an abandoned ${options.resourceLabel} lock`,
1835
- { age, holderGone, unattributable }
1836
- );
1837
- await rm(file, { force: true }).catch(() => {
1838
- });
1839
- continue;
1840
- }
1841
- if (Date.now() > deadline) {
1842
- throw new Error(
1843
- `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."
1844
- );
1845
- }
1846
- await new Promise(
1847
- (resolvePromise) => setTimeout(resolvePromise, pollMs)
1848
- );
1849
- }
1850
- }
1851
- };
1852
- return (operation) => {
1853
- const guarded = async () => {
1854
- const release = await acquire();
1855
- try {
1856
- return await operation();
1857
- } finally {
1858
- await release();
1859
- }
1860
- };
1861
- const run = mutationChain.then(guarded, guarded);
1862
- mutationChain = run.then(
1863
- () => void 0,
1864
- () => void 0
1865
- );
1866
- return run;
1867
- };
1868
- }
1869
-
1870
- // server/utils/harness/local/runtime-lifecycle.ts
1871
- var OPERATION_FILE = ".mcpjam-operation.json";
1872
- var USE_DIR = ".mcpjam-inuse";
1873
- var STAGING_PREFIX = ".mcpjam-tmp-";
1874
- var STAGING_OWNER_FILE = ".mcpjam-staging-owner.json";
1875
- function operationKeyString(key) {
1876
- return [
1877
- key.runtimeRoot,
1878
- key.harnessId,
1879
- key.target,
1880
- key.packVersion,
1881
- key.treeDigest
1882
- ].join("\0");
1883
- }
1884
- function installRootOf(key) {
1885
- return join4(key.runtimeRoot, key.target);
1886
- }
1887
- var locksByRoot = /* @__PURE__ */ new Map();
1888
- function lockFor(key) {
1889
- const root = installRootOf(key);
1890
- let lock = locksByRoot.get(root);
1891
- if (lock === void 0) {
1892
- lock = createLocalStateMutationLock({
1893
- rootDir: () => root,
1894
- lockFileName: ".mcpjam-runtime-lifecycle.lock",
1895
- resourceLabel: "runtime lifecycle",
1896
- // Short by construction: every critical section here is a read, a
1897
- // compare and an atomic write. A caller waiting longer than this is
1898
- // waiting on something that is not doing that.
1899
- timeoutMs: 15e3
1900
- });
1901
- locksByRoot.set(root, lock);
1902
- }
1903
- return lock;
1904
- }
1905
- function ownerProvablyGone(record) {
1906
- if (record.ownerPid === process.pid) {
1907
- return record.ownerStartedAt !== processStartedAt();
1908
- }
1909
- try {
1910
- process.kill(record.ownerPid, 0);
1911
- return false;
1912
- } catch (error) {
1913
- return error.code === "ESRCH";
1914
- }
1915
- }
1916
- var cachedStartedAt = null;
1917
- function processStartedAt() {
1918
- if (cachedStartedAt === null) {
1919
- cachedStartedAt = Math.floor((Date.now() - process.uptime() * 1e3) / 1e3);
2033
+ bridgeBundleDigest: `sha256:${"0".repeat(64)}`
1920
2034
  }
1921
- return cachedStartedAt;
1922
- }
1923
- async function writeJsonAtomic(file, value) {
1924
- const tmp = `${file}.${process.pid}.${randomUUID2()}.tmp`;
1925
- await writeFile2(tmp, `${JSON.stringify(value, null, 2)}
1926
- `, { mode: 384 });
1927
- await rename(tmp, file);
1928
- }
1929
- async function readJson(file) {
1930
- try {
1931
- return JSON.parse(await readFile4(file, "utf8"));
1932
- } catch {
2035
+ };
2036
+ function localPermissionModeFor(harnessId, permissionProfile, targetKind, scope = "attended") {
2037
+ const manifests = LOCAL_HARNESS_MANIFEST;
2038
+ const manifest = Object.prototype.hasOwnProperty.call(manifests, harnessId) ? manifests[harnessId] : void 0;
2039
+ if (manifest === void 0) return null;
2040
+ if (permissionProfile === "unrestricted" && targetKind === "local-native" && scope !== "unattended") {
1933
2041
  return null;
1934
2042
  }
2043
+ return manifest.permissionProfileMapping[permissionProfile] ?? null;
1935
2044
  }
1936
- async function readRuntimeOperation(key) {
1937
- const file = join4(installRootOf(key), OPERATION_FILE);
1938
- const record = await readJson(file);
1939
- if (record === null) return null;
1940
- if (record.packVersion !== key.packVersion || record.treeDigest !== key.treeDigest || record.harnessId !== key.harnessId) {
1941
- return null;
1942
- }
1943
- const terminal = record.state === "ready" || record.state === "failed" || record.state === "interrupted";
1944
- if (terminal || !ownerProvablyGone(record)) return record;
1945
- return lockFor(key)(async () => {
1946
- const fresh = await readJson(file);
1947
- if (fresh === null) return null;
1948
- if (fresh.attemptId !== record.attemptId) return fresh;
1949
- if (fresh.state === "ready" || fresh.state === "failed" || fresh.state === "interrupted") {
1950
- return fresh;
1951
- }
1952
- if (!ownerProvablyGone(fresh)) return fresh;
1953
- const reconciled = {
1954
- ...fresh,
1955
- state: "interrupted",
1956
- message: "the Inspector that started this install is no longer running, so the attempt was abandoned partway through",
1957
- updatedAt: Date.now()
2045
+ function resolveLocalCompatibility(query, manifests = LOCAL_HARNESS_MANIFEST) {
2046
+ const manifest = Object.prototype.hasOwnProperty.call(
2047
+ manifests,
2048
+ query.harnessId
2049
+ ) ? manifests[query.harnessId] : void 0;
2050
+ if (!manifest) {
2051
+ return {
2052
+ ok: false,
2053
+ status: "harness-not-supported",
2054
+ message: `${query.harnessId} has no reviewed local compatibility manifest. Run it hosted, or add a manifest entry with conformance evidence.`
1958
2055
  };
1959
- await writeJsonAtomic(file, reconciled);
1960
- logger.info("[local-harness] recovered an interrupted install attempt", {
1961
- attemptId: reconciled.attemptId,
1962
- packVersion: reconciled.packVersion
1963
- });
1964
- return reconciled;
1965
- });
1966
- }
1967
- async function beginInstallAttempt(key) {
1968
- const root = installRootOf(key);
1969
- await mkdir2(root, { recursive: true, mode: 448 });
1970
- const file = join4(root, OPERATION_FILE);
1971
- return lockFor(key)(async () => {
1972
- const existing = await readJson(file);
1973
- if (existing !== null && existing.packVersion === key.packVersion && existing.treeDigest === key.treeDigest && existing.harnessId === key.harnessId) {
1974
- const live = existing.state !== "ready" && existing.state !== "failed" && existing.state !== "interrupted";
1975
- if (live && !ownerProvablyGone(existing)) {
1976
- return { kind: "joined", record: existing };
1977
- }
1978
- }
1979
- const record = {
1980
- attemptId: `att_${randomUUID2()}`,
1981
- ownerPid: process.pid,
1982
- ownerStartedAt: processStartedAt(),
1983
- harnessId: key.harnessId,
1984
- target: key.target,
1985
- packVersion: key.packVersion,
1986
- treeDigest: key.treeDigest,
1987
- state: "reserved",
1988
- startedAt: Date.now(),
1989
- updatedAt: Date.now()
2056
+ }
2057
+ if (query.installedAdapterVersion !== manifest.adapterVersion) {
2058
+ return {
2059
+ ok: false,
2060
+ status: "adapter-version-mismatch",
2061
+ 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.`
1990
2062
  };
1991
- await writeJsonAtomic(file, record);
1992
- return { kind: "started", record };
1993
- });
1994
- }
1995
- async function updateInstallAttempt(key, attemptId, patch) {
1996
- const file = join4(installRootOf(key), OPERATION_FILE);
1997
- return lockFor(key)(async () => {
1998
- const existing = await readJson(file);
1999
- if (existing === null || existing.attemptId !== attemptId) return false;
2000
- await writeJsonAtomic(file, {
2001
- ...existing,
2002
- ...patch,
2003
- updatedAt: Date.now()
2004
- });
2005
- return true;
2006
- });
2007
- }
2008
- async function attemptStillOwns(key, attemptId) {
2009
- const record = await readJson(
2010
- join4(installRootOf(key), OPERATION_FILE)
2011
- );
2012
- return record?.attemptId === attemptId;
2013
- }
2014
- async function reserveRuntimeUse(args) {
2015
- const dir = join4(installRootOf(args.key), USE_DIR);
2016
- await mkdir2(dir, { recursive: true, mode: 448 });
2017
- const file = join4(dir, `${process.pid}-${randomUUID2()}.json`);
2018
- const record = {
2019
- pid: process.pid,
2020
- startedAt: processStartedAt(),
2021
- runtimeRoot: args.runtimeRoot,
2022
- at: Date.now(),
2023
- ...args.label ? { label: args.label } : {}
2024
- };
2025
- await writeJsonAtomic(file, record);
2026
- let released = false;
2027
- return {
2028
- release: async () => {
2029
- if (released) return;
2030
- released = true;
2031
- await rm2(file, { force: true }).catch(() => {
2032
- });
2033
- }
2034
- };
2035
- }
2036
- async function runtimeUseState(args) {
2037
- const dir = join4(installRootOf(args.key), USE_DIR);
2038
- let entries;
2039
- try {
2040
- entries = await readdir3(dir);
2041
- } catch {
2042
- return { busy: false, holders: [] };
2043
2063
  }
2044
- const holders = [];
2045
- for (const entry of entries) {
2046
- if (!entry.endsWith(".json")) continue;
2047
- const file = join4(dir, entry);
2048
- const record = await readJson(file);
2049
- if (record === null) {
2050
- continue;
2051
- }
2052
- if (record.runtimeRoot !== args.runtimeRoot) continue;
2053
- if (ownerProvablyGone({ ownerPid: record.pid, ownerStartedAt: record.startedAt })) {
2054
- await rm2(file, { force: true }).catch(() => {
2055
- });
2056
- continue;
2057
- }
2058
- holders.push(record.pid);
2064
+ if (manifest.lifecycleConformanceVersion === "") {
2065
+ return {
2066
+ ok: false,
2067
+ status: "conformance-missing",
2068
+ 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.`
2069
+ };
2059
2070
  }
2060
- return { busy: holders.length > 0, holders };
2061
- }
2062
- async function claimStagingDirectory(args) {
2063
- await writeJsonAtomic(join4(args.staging, STAGING_OWNER_FILE), {
2064
- pid: process.pid,
2065
- startedAt: processStartedAt(),
2066
- attemptId: args.attemptId,
2067
- at: Date.now()
2068
- });
2069
- }
2070
- async function sweepAbandonedStaging(key) {
2071
- const root = installRootOf(key);
2072
- let entries;
2073
- try {
2074
- entries = await readdir3(root);
2075
- } catch {
2076
- return { removed: 0, left: 0 };
2071
+ if (query.platform === null) {
2072
+ return {
2073
+ ok: false,
2074
+ status: "platform-not-supported",
2075
+ // NOT "run the harness hosted instead". A requested-local turn has Send
2076
+ // disabled in this state (`localHarnessBlocksSend`), so nothing falls
2077
+ // back — and on a local Inspector with no cloud data plane there is no
2078
+ // hosted target to switch to either, which makes the instruction
2079
+ // unactionable exactly where it is most likely to be read.
2080
+ message: `local execution is not supported on this platform.`
2081
+ };
2077
2082
  }
2078
- let removed = 0;
2079
- let left = 0;
2080
- for (const entry of entries) {
2081
- if (!entry.startsWith(STAGING_PREFIX)) continue;
2082
- const path = join4(root, entry);
2083
- const owner = await readJson(join4(path, STAGING_OWNER_FILE));
2084
- if (owner === null) {
2085
- left += 1;
2086
- continue;
2083
+ if (query.targetKind === "local-native") {
2084
+ if (!manifest.nativePlatforms.includes(query.platform)) {
2085
+ 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(", ")}).`;
2086
+ return {
2087
+ ok: false,
2088
+ status: "native-not-eligible",
2089
+ message: reason
2090
+ };
2087
2091
  }
2088
- if (!ownerProvablyGone({ ownerPid: owner.pid, ownerStartedAt: owner.startedAt })) {
2089
- left += 1;
2090
- continue;
2092
+ } else {
2093
+ if (query.backend === void 0) {
2094
+ return {
2095
+ ok: false,
2096
+ status: "backend-not-verified",
2097
+ message: "an isolated target must name its isolation backend."
2098
+ };
2099
+ }
2100
+ const verifiedHere = manifest.isolatedBackends[query.platform] ?? [];
2101
+ if (!verifiedHere.includes(query.backend)) {
2102
+ return {
2103
+ ok: false,
2104
+ status: "backend-not-verified",
2105
+ 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.`
2106
+ };
2091
2107
  }
2092
- await rm2(path, { recursive: true, force: true }).catch(() => {
2093
- });
2094
- removed += 1;
2095
2108
  }
2096
- if (removed > 0 || left > 0) {
2097
- logger.debug("[local-harness] staging sweep", { removed, left });
2109
+ const permissionMode = manifest.permissionProfileMapping[query.permissionProfile];
2110
+ if (permissionMode === void 0) {
2111
+ const offered = Object.keys(manifest.permissionProfileMapping);
2112
+ return {
2113
+ ok: false,
2114
+ status: "permission-profile-not-supported",
2115
+ 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.`)
2116
+ };
2098
2117
  }
2099
- return { removed, left };
2100
- }
2101
- async function recoverInterruptedActivation(args) {
2102
- const previous = `${args.versionRoot}${PREVIOUS_SUFFIX}`;
2103
- const hasPrevious = await stat3(previous).then(
2104
- (info) => info.isDirectory(),
2105
- () => false
2106
- );
2107
- if (!hasPrevious) return false;
2108
- return lockFor(args.key)(async () => {
2109
- const current = await stat3(args.versionRoot).then(
2110
- (info) => info.isDirectory(),
2111
- () => false
2112
- );
2113
- if (current) {
2114
- await rm2(previous, { recursive: true, force: true }).catch(() => {
2115
- });
2116
- return false;
2117
- }
2118
- await rename(previous, args.versionRoot);
2119
- logger.warn("[local-harness] restored a runtime after an interrupted activation", {
2120
- packVersion: args.key.packVersion
2121
- });
2122
- return true;
2123
- });
2124
- }
2125
- var PREVIOUS_SUFFIX = ".mcpjam-previous";
2126
- function withRuntimeLifecycleLock(key, operation) {
2127
- return lockFor(key)(operation);
2118
+ if (query.scope !== "unattended" && query.permissionProfile === "unrestricted" && query.targetKind === "local-native") {
2119
+ return {
2120
+ ok: false,
2121
+ status: "permission-profile-not-supported",
2122
+ 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.`
2123
+ };
2124
+ }
2125
+ return { ok: true, manifest, permissionMode };
2128
2126
  }
2129
2127
 
2130
2128
  // server/utils/harness/local/runtime-install.ts
@@ -2169,7 +2167,7 @@ function packSourceFor(packVersion, platformKey) {
2169
2167
  }
2170
2168
  return {
2171
2169
  kind: "url",
2172
- location: `https://github.com/MCPJam/inspector/releases/download/v${packVersion}/local-harness-pack-${platformKey}-${packVersion}.tar.gz`
2170
+ location: `https://github.com/MCPJam/inspector/releases/download/local-harness-pack-v${packVersion}/local-harness-pack-${platformKey}-${packVersion}.tar.gz`
2173
2171
  };
2174
2172
  }
2175
2173
  function packPlatformKey(platform = process.platform, arch = process.arch) {
@@ -2875,4 +2873,4 @@ export {
2875
2873
  installRuntimePack,
2876
2874
  reportLocalHarnessRuntimeStatusInBackground
2877
2875
  };
2878
- //# sourceMappingURL=chunk-WGJHSGJP.js.map
2876
+ //# sourceMappingURL=chunk-QA6VJT6A.js.map