@mcpjam/inspector 3.12.4 → 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.
- package/README.md +29 -21
- package/bin/access-link.mjs +53 -0
- package/bin/start.js +47 -14
- package/dist/client/assets/OAuthDesktopReturnNotice-B_x9xTE-.css +2 -0
- package/dist/client/assets/OAuthDesktopReturnNotice-LlC451lX.js +11 -0
- package/dist/client/assets/{app-bootstrap-B7TKDg4p.js → app-bootstrap-2jatN7gw.js} +363 -361
- package/dist/client/assets/{button-BgMj96d7.js → button-B2OH2ei2.js} +2 -2
- package/dist/client/assets/{chunk-YOKDWASO-DiKLTu3U.js → chunk-YOKDWASO-uJ3KOQV7.js} +5 -5
- package/dist/client/assets/{dead-clicks-autocapture-B2D_6BXS.js → dead-clicks-autocapture-D055GjVp.js} +2 -2
- package/dist/client/assets/{error-reporting-6uZvG5HZ.js → error-reporting-DGP36KLx.js} +57 -57
- package/dist/client/assets/{error-reporting-xR8v_8my.js → error-reporting-FWZKbZxG.js} +1 -1
- package/dist/client/assets/{exception-autocapture-Xx2wbAOf.js → exception-autocapture-103gdJjL.js} +2 -2
- package/dist/client/assets/{failed-request-tracker-BH4w1sBR.js → failed-request-tracker-Cp9_VFMm.js} +2 -2
- package/dist/client/assets/{highlighted-body-KPVGNVTW-DIgOE9D7.js → highlighted-body-KPVGNVTW-Dx70h1nm.js} +2 -2
- package/dist/client/assets/index-rskHnM3O.js +4 -0
- package/dist/client/assets/{jszip.min-C24J_3Y_.js → jszip.min-CwJyI5tb.js} +2 -2
- package/dist/client/assets/{mermaid-HWGCJPDP-BTzLBCkg.js → mermaid-HWGCJPDP-Ck3prJKl.js} +1 -1
- package/dist/client/assets/{posthog-recorder-CEtP2mh8.js → posthog-recorder-tKuMFijV.js} +2 -2
- package/dist/client/assets/{preload-helper-Cfn3cu1N.js → preload-helper-DrcoTQ49.js} +2 -2
- package/dist/client/assets/{rolldown-runtime-BPNN4GNu.js → rolldown-runtime-7FnpGOz6.js} +1 -1
- package/dist/client/assets/{surveys-CWniVjxJ.js → surveys-BsZYxM6N.js} +2 -2
- package/dist/client/assets/{trace-timeline-D-nhBNaR.js → trace-timeline-BOZvHcOG.js} +1 -1
- package/dist/client/assets/{trace-timeline-D9qZRjVv.js → trace-timeline-Bf9Oai70.js} +8 -8
- package/dist/client/assets/{web-vitals-zTwn9Gi6.js → web-vitals-DdzaVN3H.js} +2 -2
- package/dist/client/index.html +7 -7
- package/dist/server/chunk-3EDQFXTF.js +4801 -0
- package/dist/server/{chunk-5RE7KJ2M.js → chunk-4PYR7GMQ.js} +16 -197
- package/dist/server/{chunk-5T6DFO27.js → chunk-QA6VJT6A.js} +961 -963
- package/dist/server/chunk-SH24KJBI.js +194 -0
- package/dist/server/{consent-lifetime-PTGAZQSU.js → consent-lifetime-M7PS6RUU.js} +3 -2
- package/dist/server/harness-install-cli.js +1 -1
- package/dist/server/index.js +14093 -15925
- package/dist/server/local-turn-XXOSZHMT.js +12 -0
- package/package.json +14 -11
- package/dist/client/assets/OAuthDesktopReturnNotice-B2D5moE-.css +0 -2
- package/dist/client/assets/OAuthDesktopReturnNotice-CTGRhDuo.js +0 -11
- package/dist/client/assets/index-Cu4ySOft.js +0 -4
|
@@ -37,7 +37,7 @@ function resolveEnvironment() {
|
|
|
37
37
|
}
|
|
38
38
|
return "dev";
|
|
39
39
|
}
|
|
40
|
-
var BAKED_VERSION = "3.12.
|
|
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-
|
|
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/
|
|
477
|
-
import {
|
|
478
|
-
import {
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
495
|
-
|
|
424
|
+
function lockHolderProvablyGone(body) {
|
|
425
|
+
if (body === null || body.pid === process.pid) return false;
|
|
496
426
|
try {
|
|
497
|
-
|
|
427
|
+
process.kill(body.pid, 0);
|
|
428
|
+
return false;
|
|
498
429
|
} catch (error) {
|
|
499
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
578
|
-
|
|
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
|
-
|
|
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(
|
|
549
|
+
process.kill(record.ownerPid, 0);
|
|
550
|
+
return false;
|
|
586
551
|
} catch (error) {
|
|
587
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
709
|
+
async function sweepAbandonedStaging(key) {
|
|
710
|
+
const root = installRootOf(key);
|
|
835
711
|
let entries;
|
|
836
712
|
try {
|
|
837
|
-
entries = await readdir(
|
|
713
|
+
entries = await readdir(root);
|
|
838
714
|
} catch {
|
|
839
|
-
return
|
|
715
|
+
return { removed: 0, left: 0 };
|
|
840
716
|
}
|
|
841
|
-
let
|
|
717
|
+
let removed = 0;
|
|
718
|
+
let left = 0;
|
|
842
719
|
for (const entry of entries) {
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
blind = true;
|
|
727
|
+
if (!ownerProvablyGone({ ownerPid: owner.pid, ownerStartedAt: owner.startedAt })) {
|
|
728
|
+
left += 1;
|
|
855
729
|
continue;
|
|
856
730
|
}
|
|
857
|
-
|
|
858
|
-
|
|
731
|
+
await rm2(path, { recursive: true, force: true }).catch(() => {
|
|
732
|
+
});
|
|
733
|
+
removed += 1;
|
|
859
734
|
}
|
|
860
|
-
|
|
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
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
898
|
-
if (error.code === "ESRCH") return true;
|
|
899
|
-
return false;
|
|
900
|
-
}
|
|
762
|
+
});
|
|
901
763
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
1507
|
+
const root = join4(runtimeRoot, policy.bundleName);
|
|
1130
1508
|
let canonicalRoot;
|
|
1131
1509
|
try {
|
|
1132
1510
|
canonicalRoot = await realpath(root);
|
|
1133
|
-
const info = await
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
1862
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1485
1863
|
for (const entry of entries) {
|
|
1486
|
-
const full =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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-
|
|
2876
|
+
//# sourceMappingURL=chunk-QA6VJT6A.js.map
|