@algosuite/vo-mcp 0.2.0-beta.13 → 0.2.0-beta.14
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/dist/runner-cli.js +530 -135
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +557 -103
- package/dist/runner-supervisor.js.map +4 -4
- package/package.json +1 -1
|
@@ -33,8 +33,8 @@ var init_control_plane_auth_stub = __esm({
|
|
|
33
33
|
import { spawn } from "node:child_process";
|
|
34
34
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
35
35
|
import { createRequire } from "node:module";
|
|
36
|
-
import { fileURLToPath as
|
|
37
|
-
import { dirname as
|
|
36
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
37
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
38
38
|
|
|
39
39
|
// ../../scripts/virtual-office/code-runner/control-plane-client.mjs
|
|
40
40
|
var cachedFirebaseToken = null;
|
|
@@ -193,7 +193,6 @@ function createControlPlaneClient({
|
|
|
193
193
|
const json = await res.json();
|
|
194
194
|
return { task: json && json.task };
|
|
195
195
|
},
|
|
196
|
-
/** Read the current task (cancel detection). Null on 404. */
|
|
197
196
|
async getTask(taskId) {
|
|
198
197
|
const res = await req("GET", `/api/v1/code-task/${taskId}`);
|
|
199
198
|
if (res.status === 404) return null;
|
|
@@ -201,7 +200,13 @@ function createControlPlaneClient({
|
|
|
201
200
|
const json = await res.json();
|
|
202
201
|
return json ? json.task : null;
|
|
203
202
|
},
|
|
204
|
-
|
|
203
|
+
async downloadTaskAttachment(taskId, attachmentId) {
|
|
204
|
+
const path = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
|
|
205
|
+
const res = await req("GET", path);
|
|
206
|
+
if (res.status === 401) cachedFirebaseToken = null;
|
|
207
|
+
if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
208
|
+
return Buffer.from(await res.arrayBuffer());
|
|
209
|
+
},
|
|
205
210
|
async getTaskKnowledgeContext(taskId, { query } = {}) {
|
|
206
211
|
const body = {};
|
|
207
212
|
if (typeof query === "string" && query.trim()) body.query = query;
|
|
@@ -252,12 +257,17 @@ function createControlPlaneClient({
|
|
|
252
257
|
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
253
258
|
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
254
259
|
*/
|
|
255
|
-
async postHeartbeat({ runnerId: runnerId2, runnerInstanceId, operatorId, uptimeSec, activeTasks, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
260
|
+
async postHeartbeat({ runnerId: runnerId2, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
256
261
|
const body = { runner_id: runnerId2 };
|
|
257
262
|
if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
|
|
258
263
|
if (operatorId) body.operator_id = operatorId;
|
|
259
264
|
if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
|
|
260
265
|
if (typeof activeTasks === "number") body.active_tasks = activeTasks;
|
|
266
|
+
if (typeof maxConcurrency === "number") body.max_concurrency = maxConcurrency;
|
|
267
|
+
if (typeof effectiveConcurrency === "number") body.effective_concurrency = effectiveConcurrency;
|
|
268
|
+
if (typeof measuredTaskSlots === "number") body.measured_task_slots = measuredTaskSlots;
|
|
269
|
+
if (typeof measuredCpuSlots === "number") body.measured_cpu_slots = measuredCpuSlots;
|
|
270
|
+
if (typeof measuredMemorySlots === "number") body.measured_memory_slots = measuredMemorySlots;
|
|
261
271
|
if (version) body.version = version;
|
|
262
272
|
if (daemonVersion) body.daemon_version = daemonVersion;
|
|
263
273
|
if (defaultAgent) body.default_agent = defaultAgent;
|
|
@@ -284,9 +294,8 @@ function createControlPlaneClient({
|
|
|
284
294
|
throw new Error("heartbeat unauthorized (401)");
|
|
285
295
|
}
|
|
286
296
|
if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
|
|
287
|
-
return
|
|
297
|
+
return res.json();
|
|
288
298
|
},
|
|
289
|
-
/** Read the server-authoritative heartbeat ledger without mutating it. */
|
|
290
299
|
async getRunnerStatus({ operatorId } = {}) {
|
|
291
300
|
const query = operatorId ? `?operator_id=${encodeURIComponent(operatorId)}` : "";
|
|
292
301
|
const res = await req("GET", `/api/v1/runner/status${query}`, void 0, {
|
|
@@ -300,7 +309,6 @@ function createControlPlaneClient({
|
|
|
300
309
|
const body = await res.json();
|
|
301
310
|
return Array.isArray(body?.runners) ? body.runners : [];
|
|
302
311
|
},
|
|
303
|
-
/** Poll one authenticated runner's durable Mission Control action queue. */
|
|
304
312
|
async pollRunnerControl({ runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities }) {
|
|
305
313
|
const body = { runner_id: runnerId2 };
|
|
306
314
|
if (operatorId) body.operator_id = operatorId;
|
|
@@ -317,7 +325,6 @@ function createControlPlaneClient({
|
|
|
317
325
|
const action = json?.action;
|
|
318
326
|
return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
|
|
319
327
|
},
|
|
320
|
-
/** Acknowledge a maintenance action after the host has restarted the child. */
|
|
321
328
|
async completeRunnerControl(actionId, { runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities, status, detail }) {
|
|
322
329
|
const body = { runner_id: runnerId2, status };
|
|
323
330
|
if (operatorId) body.operator_id = operatorId;
|
|
@@ -492,69 +499,486 @@ function runHostMaintenance(kind, {
|
|
|
492
499
|
}
|
|
493
500
|
|
|
494
501
|
// src/runner/bundled-runtime-updater.mjs
|
|
495
|
-
import { createHash as
|
|
502
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
496
503
|
import {
|
|
497
|
-
existsSync as
|
|
498
|
-
lstatSync as
|
|
504
|
+
existsSync as existsSync4,
|
|
505
|
+
lstatSync as lstatSync3,
|
|
499
506
|
mkdirSync as mkdirSync2,
|
|
500
|
-
readFileSync as
|
|
501
|
-
readdirSync as
|
|
507
|
+
readFileSync as readFileSync4,
|
|
508
|
+
readdirSync as readdirSync3,
|
|
502
509
|
renameSync as renameSync2,
|
|
503
510
|
rmSync as rmSync2,
|
|
504
511
|
writeFileSync as writeFileSync2
|
|
505
512
|
} from "node:fs";
|
|
506
|
-
import { basename, isAbsolute as
|
|
513
|
+
import { basename, isAbsolute as isAbsolute3, join as join3, resolve as resolve4 } from "node:path";
|
|
507
514
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
508
515
|
|
|
516
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-authorization.mjs
|
|
517
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
518
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
519
|
+
import { dirname, posix, resolve as resolve2 } from "node:path";
|
|
520
|
+
import { fileURLToPath } from "node:url";
|
|
521
|
+
|
|
522
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-staged-tree.mjs
|
|
523
|
+
import { execFileSync } from "node:child_process";
|
|
524
|
+
import { createHash } from "node:crypto";
|
|
525
|
+
import {
|
|
526
|
+
existsSync as existsSync2,
|
|
527
|
+
lstatSync,
|
|
528
|
+
readFileSync,
|
|
529
|
+
readdirSync
|
|
530
|
+
} from "node:fs";
|
|
531
|
+
import { isAbsolute, join, relative, resolve, sep, win32 as win322 } from "node:path";
|
|
532
|
+
var WINDOWS_REPARSE_ATTRIBUTE = 1024;
|
|
533
|
+
var RUNNER_LOCK_KEY = "node_modules/@algosuite/vo-mcp";
|
|
534
|
+
var INSTALLED_TREE_ALGORITHM = "algohq-node-modules-manifest-sha256-v1";
|
|
535
|
+
function canonical(value) {
|
|
536
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
537
|
+
if (value && typeof value === "object") {
|
|
538
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
function canonicalJson(value) {
|
|
543
|
+
return JSON.stringify(canonical(value));
|
|
544
|
+
}
|
|
545
|
+
function hasFileAttribute(value, bit) {
|
|
546
|
+
if (typeof value === "bigint") return (value & BigInt(bit)) !== 0n;
|
|
547
|
+
return Number.isSafeInteger(value) && (value & bit) !== 0;
|
|
548
|
+
}
|
|
549
|
+
function isReparseStat(stats) {
|
|
550
|
+
if (!stats || typeof stats !== "object") return false;
|
|
551
|
+
if (typeof stats.isSymbolicLink === "function" && stats.isSymbolicLink()) return true;
|
|
552
|
+
if (stats.reparseTag !== void 0 && stats.reparseTag !== null && stats.reparseTag !== 0) return true;
|
|
553
|
+
return ["fileAttributes", "fileAttribute", "attributes"].some((key) => hasFileAttribute(stats[key], WINDOWS_REPARSE_ATTRIBUTE));
|
|
554
|
+
}
|
|
555
|
+
function platformConstraintAllows(values, target) {
|
|
556
|
+
if (!Array.isArray(values) || values.length === 0) return true;
|
|
557
|
+
if (values.some((value) => value === `!${target}`)) return false;
|
|
558
|
+
const positive = values.filter((value) => typeof value === "string" && !value.startsWith("!"));
|
|
559
|
+
return positive.length === 0 || positive.includes(target);
|
|
560
|
+
}
|
|
561
|
+
function isOmittedOptionalPackage(entry, platform = { os: "win32", arch: "x64" }) {
|
|
562
|
+
return entry?.optional === true && (!platformConstraintAllows(entry.os, platform.os) || !platformConstraintAllows(entry.cpu, platform.arch));
|
|
563
|
+
}
|
|
564
|
+
function assertContained(root, candidate, label) {
|
|
565
|
+
const rel = relative(root, candidate);
|
|
566
|
+
if (rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)) return;
|
|
567
|
+
throw new Error(`staged runtime ${label} escapes the payload root`);
|
|
568
|
+
}
|
|
569
|
+
function normalizeReportedPath(root, candidate) {
|
|
570
|
+
const absolute = resolve(String(candidate || ""));
|
|
571
|
+
assertContained(root, absolute, "reparse point");
|
|
572
|
+
return absolute;
|
|
573
|
+
}
|
|
574
|
+
function listWindowsReparsePoints(root, {
|
|
575
|
+
execFile = execFileSync,
|
|
576
|
+
env = process.env,
|
|
577
|
+
platform = process.platform
|
|
578
|
+
} = {}) {
|
|
579
|
+
if (platform !== "win32") return [];
|
|
580
|
+
const systemRoot = String(env.SystemRoot || env.SYSTEMROOT || "");
|
|
581
|
+
if (!win322.isAbsolute(systemRoot) || win322.normalize(systemRoot) !== systemRoot) {
|
|
582
|
+
throw new Error("staged runtime trusted PowerShell root unavailable");
|
|
583
|
+
}
|
|
584
|
+
const system32 = win322.join(systemRoot, "System32");
|
|
585
|
+
const powershell = win322.join(system32, "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
586
|
+
const script = [
|
|
587
|
+
'$ErrorActionPreference = "Stop"',
|
|
588
|
+
"$root = [IO.Path]::GetFullPath($env:ALGOHQ_REPARSE_ROOT)",
|
|
589
|
+
"$items = @((Get-Item -LiteralPath $root -Force)) + @(Get-ChildItem -LiteralPath $root -Force -Recurse)",
|
|
590
|
+
"foreach ($item in $items) {",
|
|
591
|
+
" if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {",
|
|
592
|
+
" [Console]::Out.WriteLine($item.FullName)",
|
|
593
|
+
" }",
|
|
594
|
+
"}"
|
|
595
|
+
].join("; ");
|
|
596
|
+
const output = execFile(powershell, [
|
|
597
|
+
"-NoLogo",
|
|
598
|
+
"-NoProfile",
|
|
599
|
+
"-NonInteractive",
|
|
600
|
+
"-Command",
|
|
601
|
+
script
|
|
602
|
+
], {
|
|
603
|
+
cwd: system32,
|
|
604
|
+
encoding: "utf8",
|
|
605
|
+
env: { SystemRoot: systemRoot, ALGOHQ_REPARSE_ROOT: win322.resolve(root) },
|
|
606
|
+
windowsHide: true
|
|
607
|
+
});
|
|
608
|
+
return String(output || "").split(/\r?\n/u).filter(Boolean).map((item) => normalizeReportedPath(root, item));
|
|
609
|
+
}
|
|
610
|
+
function normalizedRunnerRecord(actual, expected) {
|
|
611
|
+
const normalized = structuredClone(actual);
|
|
612
|
+
if (normalized?.resolved === expected?.resolved) return normalized;
|
|
613
|
+
if (typeof normalized?.resolved !== "string" || !normalized.resolved.startsWith("file:")) {
|
|
614
|
+
return normalized;
|
|
615
|
+
}
|
|
616
|
+
const fileName = normalized.resolved.slice("file:".length).replaceAll("\\", "/").split("/").at(-1);
|
|
617
|
+
const expectedFileNames = /* @__PURE__ */ new Set([
|
|
618
|
+
String(expected.resolved || "").split("/").at(-1),
|
|
619
|
+
`algosuite-vo-mcp-${expected.version}.tgz`
|
|
620
|
+
]);
|
|
621
|
+
if (expectedFileNames.has(fileName)) normalized.resolved = expected.resolved;
|
|
622
|
+
return normalized;
|
|
623
|
+
}
|
|
624
|
+
function validateLock(lock, authorization) {
|
|
625
|
+
if (!lock || typeof lock !== "object" || Array.isArray(lock)) {
|
|
626
|
+
throw new Error("staged runtime package-lock must be an object");
|
|
627
|
+
}
|
|
628
|
+
if (lock.lockfileVersion !== authorization.dependency_lock.source_lockfile_version) {
|
|
629
|
+
throw new Error("staged runtime package-lock version mismatch");
|
|
630
|
+
}
|
|
631
|
+
if (!lock.packages || typeof lock.packages !== "object" || Array.isArray(lock.packages)) {
|
|
632
|
+
throw new Error("staged runtime package-lock package map missing");
|
|
633
|
+
}
|
|
634
|
+
const expected = authorization.dependency_lock.packages;
|
|
635
|
+
const actual = Object.fromEntries(Object.entries(lock.packages).filter(([key]) => key !== ""));
|
|
636
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
637
|
+
const actualKeys = Object.keys(actual).sort();
|
|
638
|
+
if (canonicalJson(actualKeys) !== canonicalJson(expectedKeys)) {
|
|
639
|
+
throw new Error("staged runtime package-lock package set mismatch");
|
|
640
|
+
}
|
|
641
|
+
for (const key of expectedKeys) {
|
|
642
|
+
const record = key === RUNNER_LOCK_KEY ? normalizedRunnerRecord(actual[key], expected[key]) : actual[key];
|
|
643
|
+
if (canonicalJson(record) !== canonicalJson(expected[key])) {
|
|
644
|
+
throw new Error(`staged runtime package-lock record mismatch: ${key}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return { actual, expected };
|
|
648
|
+
}
|
|
649
|
+
function checkPathKind(path, expectedKind, fsOps, label) {
|
|
650
|
+
if (!fsOps.exists(path)) throw new Error(`staged runtime ${label} missing`);
|
|
651
|
+
const stats = fsOps.lstat(path);
|
|
652
|
+
if (isReparseStat(stats) || fsOps.isReparsePoint(path, stats)) {
|
|
653
|
+
throw new Error(`staged runtime ${label} is a reparse point`);
|
|
654
|
+
}
|
|
655
|
+
if (expectedKind === "directory" && !stats.isDirectory()) {
|
|
656
|
+
throw new Error(`staged runtime ${label} is not a directory`);
|
|
657
|
+
}
|
|
658
|
+
if (expectedKind === "file" && !stats.isFile()) {
|
|
659
|
+
throw new Error(`staged runtime ${label} is not a regular file`);
|
|
660
|
+
}
|
|
661
|
+
if (expectedKind === "file" && Number(stats.nlink) > 1) {
|
|
662
|
+
throw new Error(`staged runtime ${label} is hardlinked`);
|
|
663
|
+
}
|
|
664
|
+
return stats;
|
|
665
|
+
}
|
|
666
|
+
function verifyInstalledPackages(payloadRoot, packageRecords, platform, fsOps) {
|
|
667
|
+
const omitted = [];
|
|
668
|
+
let installed = 0;
|
|
669
|
+
for (const [key, entry] of Object.entries(packageRecords)) {
|
|
670
|
+
const path = resolve(payloadRoot, key);
|
|
671
|
+
assertContained(payloadRoot, path, "package path");
|
|
672
|
+
const shouldOmit = isOmittedOptionalPackage(entry, platform);
|
|
673
|
+
if (shouldOmit) {
|
|
674
|
+
omitted.push(key);
|
|
675
|
+
if (fsOps.exists(path)) throw new Error(`staged runtime optional package should be omitted: ${key}`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
checkPathKind(path, "directory", fsOps, `installed package ${key}`);
|
|
679
|
+
installed += 1;
|
|
680
|
+
}
|
|
681
|
+
return { installed, omitted: omitted.sort() };
|
|
682
|
+
}
|
|
683
|
+
function treeRecord(kind, path, stats, fileHash = "") {
|
|
684
|
+
if (kind === "d") return `d ${path}\r
|
|
685
|
+
`;
|
|
686
|
+
return `f ${path} ${stats.size} ${fileHash}\r
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
function computeInstalledTree(nodeModulesRoot, fsOps = {}) {
|
|
690
|
+
const ops = {
|
|
691
|
+
exists: existsSync2,
|
|
692
|
+
lstat: lstatSync,
|
|
693
|
+
readdir: (path) => readdirSync(path, { withFileTypes: true }),
|
|
694
|
+
readFile: readFileSync,
|
|
695
|
+
isReparsePoint: () => false,
|
|
696
|
+
listReparsePoints: listWindowsReparsePoints,
|
|
697
|
+
...fsOps
|
|
698
|
+
};
|
|
699
|
+
const root = resolve(nodeModulesRoot);
|
|
700
|
+
checkPathKind(root, "directory", ops, "node_modules root");
|
|
701
|
+
const reported = ops.listReparsePoints(root);
|
|
702
|
+
if (!Array.isArray(reported)) throw new Error("staged runtime reparse probe returned an invalid result");
|
|
703
|
+
if (reported.length > 0) throw new Error("staged runtime tree contains a Windows reparse point");
|
|
704
|
+
let fileCount = 0;
|
|
705
|
+
let directoryCount = 1;
|
|
706
|
+
let byteCount = 0;
|
|
707
|
+
const entries = [];
|
|
708
|
+
function walk(absolute, relativePath) {
|
|
709
|
+
for (const entry of ops.readdir(absolute)) {
|
|
710
|
+
const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
711
|
+
if (childRelative === ".package-lock.json") continue;
|
|
712
|
+
const child = resolve(absolute, entry.name);
|
|
713
|
+
assertContained(root, child, "tree entry");
|
|
714
|
+
const stats = ops.lstat(child);
|
|
715
|
+
if (isReparseStat(stats) || ops.isReparsePoint(child, stats)) {
|
|
716
|
+
throw new Error(`staged runtime tree contains a reparse point: ${childRelative}`);
|
|
717
|
+
}
|
|
718
|
+
if (stats.isDirectory()) {
|
|
719
|
+
directoryCount += 1;
|
|
720
|
+
entries.push({ kind: "d", path: childRelative, stats });
|
|
721
|
+
walk(child, childRelative);
|
|
722
|
+
} else if (stats.isFile()) {
|
|
723
|
+
if (Number(stats.nlink) > 1) {
|
|
724
|
+
throw new Error(`staged runtime tree contains a hardlinked file: ${childRelative}`);
|
|
725
|
+
}
|
|
726
|
+
fileCount += 1;
|
|
727
|
+
byteCount += Number(stats.size);
|
|
728
|
+
const digest = createHash("sha256").update(ops.readFile(child)).digest("hex");
|
|
729
|
+
entries.push({ kind: "f", path: childRelative, stats, digest });
|
|
730
|
+
} else {
|
|
731
|
+
throw new Error(`staged runtime tree contains a non-file entry: ${childRelative}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
walk(root, "");
|
|
736
|
+
entries.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
|
|
737
|
+
const manifest = treeRecord("d", "", { size: 0 }) + entries.map((entry) => treeRecord(entry.kind, entry.path, entry.stats, entry.digest)).join("");
|
|
738
|
+
return {
|
|
739
|
+
algorithm: INSTALLED_TREE_ALGORITHM,
|
|
740
|
+
sha256: createHash("sha256").update(manifest, "utf8").digest("hex"),
|
|
741
|
+
file_count: fileCount,
|
|
742
|
+
directory_count: directoryCount,
|
|
743
|
+
byte_count: byteCount,
|
|
744
|
+
canonical_manifest_byte_count: Buffer.byteLength(manifest),
|
|
745
|
+
reparse_point_count: 0,
|
|
746
|
+
hardlinked_file_count: 0
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function compareStagedRuntime({
|
|
750
|
+
payloadRoot,
|
|
751
|
+
nodeModulesRoot = join(payloadRoot, "node_modules"),
|
|
752
|
+
packageLockFile = join(payloadRoot, "package-lock.json"),
|
|
753
|
+
authorization,
|
|
754
|
+
fsOps = {}
|
|
755
|
+
}) {
|
|
756
|
+
const payload = resolve(payloadRoot);
|
|
757
|
+
const modules = resolve(nodeModulesRoot);
|
|
758
|
+
const lockFile = resolve(packageLockFile);
|
|
759
|
+
if (modules !== resolve(payload, "node_modules") || lockFile !== resolve(payload, "package-lock.json")) {
|
|
760
|
+
throw new Error("staged runtime paths do not identify one canonical payload");
|
|
761
|
+
}
|
|
762
|
+
const ops = {
|
|
763
|
+
exists: existsSync2,
|
|
764
|
+
lstat: lstatSync,
|
|
765
|
+
readFile: readFileSync,
|
|
766
|
+
isReparsePoint: () => false,
|
|
767
|
+
...fsOps
|
|
768
|
+
};
|
|
769
|
+
checkPathKind(lockFile, "file", ops, "package-lock");
|
|
770
|
+
const lock = JSON.parse(ops.readFile(lockFile, "utf8"));
|
|
771
|
+
const { expected } = validateLock(lock, authorization);
|
|
772
|
+
const platform = { os: authorization.platform.os, arch: authorization.platform.arch };
|
|
773
|
+
const packages = verifyInstalledPackages(payload, expected, platform, ops);
|
|
774
|
+
const declaredOmitted = [...authorization.dependency_lock.windows_x64_omitted_optional_entries].sort();
|
|
775
|
+
if (canonicalJson(packages.omitted) !== canonicalJson(declaredOmitted)) {
|
|
776
|
+
throw new Error("staged runtime optional omission list mismatch");
|
|
777
|
+
}
|
|
778
|
+
if (packages.installed !== Object.keys(expected).length - packages.omitted.length) {
|
|
779
|
+
throw new Error("staged runtime installed package count mismatch");
|
|
780
|
+
}
|
|
781
|
+
const tree = computeInstalledTree(modules, fsOps);
|
|
782
|
+
if (canonicalJson(tree) !== canonicalJson(authorization.installed_tree)) {
|
|
783
|
+
throw new Error("staged runtime installed tree mismatch");
|
|
784
|
+
}
|
|
785
|
+
return { ok: true, packageCount: Object.keys(expected).length, ...packages, tree };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-authorization.mjs
|
|
789
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
790
|
+
var BETA13_WINDOWS_X64_AUTHORIZATION = resolve2(
|
|
791
|
+
HERE,
|
|
792
|
+
"authorizations",
|
|
793
|
+
"vo-mcp-0.2.0-beta.13-win32-x64.json"
|
|
794
|
+
);
|
|
795
|
+
var EXPECTED = Object.freeze({
|
|
796
|
+
authorizationId: "vo-mcp-0.2.0-beta.13-win32-x64-v1",
|
|
797
|
+
packageName: "@algosuite/vo-mcp",
|
|
798
|
+
version: "0.2.0-beta.13",
|
|
799
|
+
registry: "https://registry.npmjs.org/",
|
|
800
|
+
tarballUrl: "https://registry.npmjs.org/@algosuite/vo-mcp/-/vo-mcp-0.2.0-beta.13.tgz",
|
|
801
|
+
integrity: "sha512-TQa5VaEFsaleHbAZZp+zS4u5U/0zQBIGOiPDGn9IqfZfdMltH2dY81nTftvu5EUABthE1xTCrdSzJjO9mzkNag==",
|
|
802
|
+
tarballSha256: "c1e39e8bb2df7f53f48e46e77ffb617452a01849469ff4757b4384a478c1cbff",
|
|
803
|
+
npmShasumSha1: "f076649b31294aa38deb7852f38a889e0d0fbe44",
|
|
804
|
+
gitHead: "ba00db90720416fa7474581eb823c6255221880f",
|
|
805
|
+
sourceLockSha256: "f8bbb5e81a0057ee2d60145580ec07f7e7055d999bdd038515c6c4e88af6cc92",
|
|
806
|
+
canonicalEntriesSha256: "7f6b2f93ccb93ad625ed244dc59ac556792e478178fc690ad2ca2c6f3a2325d2",
|
|
807
|
+
entryCount: 107,
|
|
808
|
+
optionalEntryCount: 13,
|
|
809
|
+
installedEntryCount: 96,
|
|
810
|
+
treeAlgorithm: "algohq-node-modules-manifest-sha256-v1",
|
|
811
|
+
treeSha256: "9c8b0749d7ae1e2c132ef08c5b5655673ae88432859c561806276c9eb18f4874",
|
|
812
|
+
treeFileCount: 3538,
|
|
813
|
+
treeDirectoryCount: 553,
|
|
814
|
+
treeByteCount: 20566445,
|
|
815
|
+
treeManifestByteCount: 412062
|
|
816
|
+
});
|
|
817
|
+
var SRI_RE = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
818
|
+
var SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
819
|
+
function canonical2(value) {
|
|
820
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
821
|
+
if (value && typeof value === "object") {
|
|
822
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical2(value[key])]));
|
|
823
|
+
}
|
|
824
|
+
return value;
|
|
825
|
+
}
|
|
826
|
+
function sha256Canonical(value) {
|
|
827
|
+
return createHash2("sha256").update(`${JSON.stringify(canonical2(value))}
|
|
828
|
+
`, "utf8").digest("hex");
|
|
829
|
+
}
|
|
830
|
+
function assertEqual(actual, expected, label) {
|
|
831
|
+
if (actual !== expected) throw new Error(`runtime authorization ${label} mismatch`);
|
|
832
|
+
}
|
|
833
|
+
function isOmittedOnWindowsX64(entry) {
|
|
834
|
+
return isOmittedOptionalPackage(entry, { os: "win32", arch: "x64" });
|
|
835
|
+
}
|
|
836
|
+
function validateRuntimeAuthorization(value) {
|
|
837
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
838
|
+
throw new Error("runtime authorization must be an object");
|
|
839
|
+
}
|
|
840
|
+
assertEqual(value.schema_version, 1, "schema version");
|
|
841
|
+
assertEqual(value.authorization_id, EXPECTED.authorizationId, "id");
|
|
842
|
+
assertEqual(value.package?.name, EXPECTED.packageName, "package name");
|
|
843
|
+
assertEqual(value.package?.version, EXPECTED.version, "package version");
|
|
844
|
+
assertEqual(value.package?.registry, EXPECTED.registry, "registry");
|
|
845
|
+
assertEqual(value.package?.tarball_url, EXPECTED.tarballUrl, "tarball URL");
|
|
846
|
+
assertEqual(value.package?.sri_sha512, EXPECTED.integrity, "package integrity");
|
|
847
|
+
assertEqual(value.package?.tarball_sha256, EXPECTED.tarballSha256, "tarball sha256");
|
|
848
|
+
assertEqual(value.package?.npm_shasum_sha1, EXPECTED.npmShasumSha1, "npm shasum");
|
|
849
|
+
assertEqual(value.package?.git_head, EXPECTED.gitHead, "git head");
|
|
850
|
+
assertEqual(value.package?.packed_file_count, 25, "packed file count");
|
|
851
|
+
assertEqual(value.package?.packed_bytes, 846151, "packed byte count");
|
|
852
|
+
assertEqual(value.package?.unpacked_bytes, 3350387, "unpacked byte count");
|
|
853
|
+
assertEqual(value.platform?.os, "win32", "operating system");
|
|
854
|
+
assertEqual(value.platform?.arch, "x64", "architecture");
|
|
855
|
+
assertEqual(value.platform?.package_node_engine, ">=22.5.0", "package node engine");
|
|
856
|
+
if (!/^24\.15\.0$/u.test(String(value.platform?.authorization_builder_node || "")) || value.platform?.authorization_builder_npm !== "11.12.1") {
|
|
857
|
+
throw new Error("runtime authorization builder toolchain mismatch");
|
|
858
|
+
}
|
|
859
|
+
for (const [key, expected] of Object.entries({
|
|
860
|
+
ignore_scripts: true,
|
|
861
|
+
bin_links: false,
|
|
862
|
+
include_optional: true,
|
|
863
|
+
omit_dev: true,
|
|
864
|
+
audit: false,
|
|
865
|
+
fund: false,
|
|
866
|
+
reject_links_reparse_points: true,
|
|
867
|
+
reject_hardlinks: true,
|
|
868
|
+
remove_generated_node_modules_package_lock_before_tree_validation: true
|
|
869
|
+
})) assertEqual(value.install_contract?.[key], expected, `install contract ${key}`);
|
|
870
|
+
assertEqual(value.install_contract?.allowed_registry_prefix, EXPECTED.registry, "registry prefix");
|
|
871
|
+
const lock = value.dependency_lock;
|
|
872
|
+
if (!lock?.packages || typeof lock.packages !== "object" || Array.isArray(lock.packages)) {
|
|
873
|
+
throw new Error("runtime authorization dependency set missing");
|
|
874
|
+
}
|
|
875
|
+
assertEqual(lock.source_lockfile_version, 3, "lockfile version");
|
|
876
|
+
assertEqual(lock.source_lock_sha256, EXPECTED.sourceLockSha256, "source lock sha256");
|
|
877
|
+
assertEqual(lock.canonical_entries_sha256, EXPECTED.canonicalEntriesSha256, "entry-set sha256");
|
|
878
|
+
assertEqual(lock.entry_count, EXPECTED.entryCount, "entry count");
|
|
879
|
+
assertEqual(lock.integrity_entry_count, EXPECTED.entryCount, "integrity count");
|
|
880
|
+
assertEqual(lock.optional_entry_count, EXPECTED.optionalEntryCount, "optional count");
|
|
881
|
+
assertEqual(lock.windows_x64_installed_entry_count, EXPECTED.installedEntryCount, "installed count");
|
|
882
|
+
const entries = Object.entries(lock.packages);
|
|
883
|
+
assertEqual(entries.length, EXPECTED.entryCount, "package map count");
|
|
884
|
+
for (const [key, entry] of entries) {
|
|
885
|
+
if (!key.startsWith("node_modules/") || key.includes("\\") || posix.normalize(key) !== key || key.split("/").includes("..")) {
|
|
886
|
+
throw new Error(`runtime authorization has unsafe package path: ${key}`);
|
|
887
|
+
}
|
|
888
|
+
if (!entry || typeof entry !== "object" || entry.link === true) {
|
|
889
|
+
throw new Error(`runtime authorization contains a linked package: ${key}`);
|
|
890
|
+
}
|
|
891
|
+
if (!SRI_RE.test(String(entry.integrity || ""))) {
|
|
892
|
+
throw new Error(`runtime authorization package lacks sha512 integrity: ${key}`);
|
|
893
|
+
}
|
|
894
|
+
if (!String(entry.resolved || "").startsWith(EXPECTED.registry)) {
|
|
895
|
+
throw new Error(`runtime authorization package is outside the public registry: ${key}`);
|
|
896
|
+
}
|
|
897
|
+
if (entry.hasInstallScript === true) {
|
|
898
|
+
throw new Error(`runtime authorization package declares an install script: ${key}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const runner = lock.packages["node_modules/@algosuite/vo-mcp"];
|
|
902
|
+
assertEqual(runner?.version, EXPECTED.version, "runner dependency version");
|
|
903
|
+
assertEqual(runner?.integrity, EXPECTED.integrity, "runner dependency integrity");
|
|
904
|
+
assertEqual(sha256Canonical(lock.packages), EXPECTED.canonicalEntriesSha256, "computed entry-set sha256");
|
|
905
|
+
const omitted = entries.filter(([, entry]) => isOmittedOnWindowsX64(entry)).map(([key]) => key).sort();
|
|
906
|
+
const declaredOmitted = [...lock.windows_x64_omitted_optional_entries || []].sort();
|
|
907
|
+
assertEqual(JSON.stringify(declaredOmitted), JSON.stringify(omitted), "omitted optional entries");
|
|
908
|
+
assertEqual(entries.length - omitted.length, EXPECTED.installedEntryCount, "derived installed count");
|
|
909
|
+
const tree = value.installed_tree;
|
|
910
|
+
assertEqual(tree?.algorithm, EXPECTED.treeAlgorithm, "tree algorithm");
|
|
911
|
+
if (!SHA256_RE.test(String(tree?.sha256 || ""))) throw new Error("runtime authorization tree hash invalid");
|
|
912
|
+
assertEqual(tree.sha256, EXPECTED.treeSha256, "tree sha256");
|
|
913
|
+
assertEqual(tree.file_count, EXPECTED.treeFileCount, "tree file count");
|
|
914
|
+
assertEqual(tree.directory_count, EXPECTED.treeDirectoryCount, "tree directory count");
|
|
915
|
+
assertEqual(tree.byte_count, EXPECTED.treeByteCount, "tree byte count");
|
|
916
|
+
assertEqual(tree.canonical_manifest_byte_count, EXPECTED.treeManifestByteCount, "tree manifest byte count");
|
|
917
|
+
assertEqual(tree.reparse_point_count, 0, "tree reparse count");
|
|
918
|
+
assertEqual(tree.hardlinked_file_count, 0, "tree hardlink count");
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
function readRuntimeAuthorization(file = BETA13_WINDOWS_X64_AUTHORIZATION) {
|
|
922
|
+
return validateRuntimeAuthorization(JSON.parse(readFileSync2(file, "utf8")));
|
|
923
|
+
}
|
|
924
|
+
function validateStagedRuntimeAuthorization(options) {
|
|
925
|
+
const authorization = validateRuntimeAuthorization(options?.authorization || readRuntimeAuthorization());
|
|
926
|
+
return compareStagedRuntime({ ...options, authorization });
|
|
927
|
+
}
|
|
928
|
+
if (process.argv[1] && resolve2(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
929
|
+
readRuntimeAuthorization(process.argv[2] ? resolve2(process.argv[2]) : void 0);
|
|
930
|
+
process.stdout.write("AlgoHQ runtime authorization valid\n");
|
|
931
|
+
}
|
|
932
|
+
|
|
509
933
|
// src/runner/bundled-runtime-store.mjs
|
|
510
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
934
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
511
935
|
import {
|
|
512
936
|
closeSync,
|
|
513
|
-
existsSync as
|
|
937
|
+
existsSync as existsSync3,
|
|
514
938
|
fsyncSync,
|
|
515
|
-
lstatSync,
|
|
939
|
+
lstatSync as lstatSync2,
|
|
516
940
|
mkdirSync,
|
|
517
941
|
openSync,
|
|
518
|
-
readFileSync,
|
|
519
|
-
readdirSync,
|
|
942
|
+
readFileSync as readFileSync3,
|
|
943
|
+
readdirSync as readdirSync2,
|
|
520
944
|
realpathSync,
|
|
521
945
|
renameSync,
|
|
522
946
|
rmSync,
|
|
523
947
|
writeFileSync
|
|
524
948
|
} from "node:fs";
|
|
525
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
949
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve3 } from "node:path";
|
|
526
950
|
var SLOT_ID_RE = /^vo-mcp-[0-9A-Za-z._-]{1,96}$/u;
|
|
527
951
|
var ACTION_ID_RE = /^[0-9A-Za-z._-]{1,128}$/u;
|
|
528
952
|
var INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
529
953
|
var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
530
954
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
531
|
-
var ENTRY_REL =
|
|
532
|
-
var SUPERVISOR_REL =
|
|
533
|
-
var PACKAGE_REL =
|
|
534
|
-
var CREDENTIAL_HELPER_REL =
|
|
955
|
+
var ENTRY_REL = join2("node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp");
|
|
956
|
+
var SUPERVISOR_REL = join2("node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js");
|
|
957
|
+
var PACKAGE_REL = join2("node_modules", "@algosuite", "vo-mcp", "package.json");
|
|
958
|
+
var CREDENTIAL_HELPER_REL = join2("node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js");
|
|
535
959
|
var MANIFEST_FILE = "runtime-manifest.json";
|
|
536
960
|
function within(parent, candidate) {
|
|
537
|
-
const rel =
|
|
538
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
961
|
+
const rel = relative2(resolve3(parent), resolve3(candidate));
|
|
962
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
539
963
|
}
|
|
540
964
|
function runtimeRootFromEnv(env = process.env) {
|
|
541
965
|
const value = String(env.VO_RUNNER_RUNTIME_ROOT || "").trim();
|
|
542
|
-
return value &&
|
|
966
|
+
return value && isAbsolute2(value) ? resolve3(value) : null;
|
|
543
967
|
}
|
|
544
968
|
function hashFileSha512(file) {
|
|
545
|
-
return `sha512-${
|
|
969
|
+
return `sha512-${createHash3("sha512").update(readFileSync3(file)).digest("base64")}`;
|
|
546
970
|
}
|
|
547
971
|
function hashRuntimeTree(root) {
|
|
548
|
-
const hasher =
|
|
972
|
+
const hasher = createHash3("sha512");
|
|
549
973
|
const files = [];
|
|
550
974
|
const visit = (directory, prefix = "") => {
|
|
551
|
-
const rootStat =
|
|
975
|
+
const rootStat = lstatSync2(directory);
|
|
552
976
|
if (rootStat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
553
977
|
if (!rootStat.isDirectory()) throw new Error("runtime tree root is not a directory");
|
|
554
|
-
for (const name of
|
|
555
|
-
const absolute =
|
|
978
|
+
for (const name of readdirSync2(directory).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) {
|
|
979
|
+
const absolute = join2(directory, name);
|
|
556
980
|
const relativePath = prefix ? `${prefix}/${name}` : name;
|
|
557
|
-
const stat =
|
|
981
|
+
const stat = lstatSync2(absolute);
|
|
558
982
|
if (stat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
559
983
|
if (stat.isDirectory()) visit(absolute, relativePath);
|
|
560
984
|
else if (stat.isFile() && relativePath !== MANIFEST_FILE) files.push({ absolute, relativePath, size: stat.size });
|
|
@@ -568,14 +992,14 @@ function hashRuntimeTree(root) {
|
|
|
568
992
|
hasher.update(`${pathBytes.length}:`);
|
|
569
993
|
hasher.update(pathBytes);
|
|
570
994
|
hasher.update(`:${file.size}:`);
|
|
571
|
-
hasher.update(
|
|
995
|
+
hasher.update(readFileSync3(file.absolute));
|
|
572
996
|
hasher.update("\n");
|
|
573
997
|
}
|
|
574
998
|
return `sha512-${hasher.digest("base64")}`;
|
|
575
999
|
}
|
|
576
1000
|
function atomicWriteJson(file, value) {
|
|
577
|
-
mkdirSync(
|
|
578
|
-
const temp =
|
|
1001
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
1002
|
+
const temp = join2(dirname2(file), `.${randomUUID()}.tmp`);
|
|
579
1003
|
const fd = openSync(temp, "wx", 384);
|
|
580
1004
|
try {
|
|
581
1005
|
writeFileSync(fd, `${JSON.stringify(value, null, 2)}
|
|
@@ -588,7 +1012,7 @@ function atomicWriteJson(file, value) {
|
|
|
588
1012
|
renameSync(temp, file);
|
|
589
1013
|
if (process.platform !== "win32") {
|
|
590
1014
|
try {
|
|
591
|
-
const parentFd = openSync(
|
|
1015
|
+
const parentFd = openSync(dirname2(file), "r");
|
|
592
1016
|
try {
|
|
593
1017
|
fsyncSync(parentFd);
|
|
594
1018
|
} finally {
|
|
@@ -602,10 +1026,10 @@ function atomicWriteJson(file, value) {
|
|
|
602
1026
|
}
|
|
603
1027
|
}
|
|
604
1028
|
function readActivation(runtimeRoot) {
|
|
605
|
-
const file =
|
|
606
|
-
if (!
|
|
1029
|
+
const file = join2(runtimeRoot, "current.json");
|
|
1030
|
+
if (!existsSync3(file)) return null;
|
|
607
1031
|
try {
|
|
608
|
-
const value = JSON.parse(
|
|
1032
|
+
const value = JSON.parse(readFileSync3(file, "utf8"));
|
|
609
1033
|
return value?.schema_version === 1 ? value : null;
|
|
610
1034
|
} catch {
|
|
611
1035
|
return null;
|
|
@@ -613,14 +1037,14 @@ function readActivation(runtimeRoot) {
|
|
|
613
1037
|
}
|
|
614
1038
|
function slotPaths(runtimeRoot, slotId) {
|
|
615
1039
|
if (!SLOT_ID_RE.test(slotId)) throw new Error("invalid runtime slot id");
|
|
616
|
-
const slotRoot =
|
|
1040
|
+
const slotRoot = join2(runtimeRoot, "slots", slotId);
|
|
617
1041
|
return {
|
|
618
1042
|
slotRoot,
|
|
619
|
-
entry:
|
|
620
|
-
supervisor:
|
|
621
|
-
packageJson:
|
|
622
|
-
credentialHelper:
|
|
623
|
-
manifest:
|
|
1043
|
+
entry: join2(slotRoot, ENTRY_REL),
|
|
1044
|
+
supervisor: join2(slotRoot, SUPERVISOR_REL),
|
|
1045
|
+
packageJson: join2(slotRoot, PACKAGE_REL),
|
|
1046
|
+
credentialHelper: join2(slotRoot, CREDENTIAL_HELPER_REL),
|
|
1047
|
+
manifest: join2(slotRoot, MANIFEST_FILE)
|
|
624
1048
|
};
|
|
625
1049
|
}
|
|
626
1050
|
function validActive(active) {
|
|
@@ -630,8 +1054,8 @@ function validateSlot(runtimeRoot, active) {
|
|
|
630
1054
|
if (!validActive(active)) return { ok: false, detail: "invalid activation metadata" };
|
|
631
1055
|
const paths = slotPaths(runtimeRoot, active.slot_id);
|
|
632
1056
|
try {
|
|
633
|
-
const manifest = JSON.parse(
|
|
634
|
-
const pkg = JSON.parse(
|
|
1057
|
+
const manifest = JSON.parse(readFileSync3(paths.manifest, "utf8"));
|
|
1058
|
+
const pkg = JSON.parse(readFileSync3(paths.packageJson, "utf8"));
|
|
635
1059
|
const expected = {
|
|
636
1060
|
slot_id: active.slot_id,
|
|
637
1061
|
version: active.version,
|
|
@@ -646,7 +1070,7 @@ function validateSlot(runtimeRoot, active) {
|
|
|
646
1070
|
if (manifest?.schema_version !== 1 || pkg?.name !== "@algosuite/vo-mcp" || pkg?.version !== active.version) {
|
|
647
1071
|
return { ok: false, detail: "package identity mismatch" };
|
|
648
1072
|
}
|
|
649
|
-
if (
|
|
1073
|
+
if (lstatSync2(paths.entry).isSymbolicLink() || lstatSync2(paths.supervisor).isSymbolicLink() || lstatSync2(paths.credentialHelper).isSymbolicLink()) {
|
|
650
1074
|
return { ok: false, detail: "runtime entry cannot be a link" };
|
|
651
1075
|
}
|
|
652
1076
|
if (!within(paths.slotRoot, realpathSync(paths.entry)) || !within(paths.slotRoot, realpathSync(paths.supervisor)) || !within(paths.slotRoot, realpathSync(paths.credentialHelper))) {
|
|
@@ -662,7 +1086,7 @@ function validateSlot(runtimeRoot, active) {
|
|
|
662
1086
|
}
|
|
663
1087
|
function journalActivation(runtimeRoot, actionId, state, detail = "") {
|
|
664
1088
|
if (!ACTION_ID_RE.test(actionId)) throw new Error("invalid runner action id");
|
|
665
|
-
atomicWriteJson(
|
|
1089
|
+
atomicWriteJson(join2(runtimeRoot, "transactions", `${actionId}.json`), {
|
|
666
1090
|
schema_version: 1,
|
|
667
1091
|
action_id: actionId,
|
|
668
1092
|
state,
|
|
@@ -693,7 +1117,7 @@ function activateSlot(runtimeRoot, active, action) {
|
|
|
693
1117
|
}
|
|
694
1118
|
};
|
|
695
1119
|
journalActivation(runtimeRoot, action.actionId, "prepared", `${active.version} ${active.integrity}`);
|
|
696
|
-
atomicWriteJson(
|
|
1120
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), pointer);
|
|
697
1121
|
return pointer;
|
|
698
1122
|
}
|
|
699
1123
|
function activationSupervisorInstanceId(runtimeRoot, fallback) {
|
|
@@ -712,7 +1136,7 @@ function recordActivationRetry(runtimeRoot, pointer, detail) {
|
|
|
712
1136
|
...current,
|
|
713
1137
|
pending: { ...current.pending, ack_attempts: attempts, last_error: String(detail).slice(0, 240) }
|
|
714
1138
|
};
|
|
715
|
-
atomicWriteJson(
|
|
1139
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), updated);
|
|
716
1140
|
journalActivation(runtimeRoot, current.pending.action_id, "ack-retry", `attempt ${attempts}: ${detail}`);
|
|
717
1141
|
return updated;
|
|
718
1142
|
}
|
|
@@ -737,7 +1161,7 @@ function finalizeActivation(runtimeRoot, pointer) {
|
|
|
737
1161
|
throw new Error("runtime activation generation changed before finalization");
|
|
738
1162
|
}
|
|
739
1163
|
journalActivation(runtimeRoot, pointer.pending.action_id, "attesting", `${pointer.active.version} verified`);
|
|
740
|
-
atomicWriteJson(
|
|
1164
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), {
|
|
741
1165
|
schema_version: 1,
|
|
742
1166
|
generation: pointer.generation,
|
|
743
1167
|
active: pointer.active,
|
|
@@ -767,7 +1191,7 @@ function rollbackActivation(runtimeRoot, pointer, detail) {
|
|
|
767
1191
|
rolled_back_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
768
1192
|
}
|
|
769
1193
|
};
|
|
770
|
-
atomicWriteJson(
|
|
1194
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), rolledBack);
|
|
771
1195
|
try {
|
|
772
1196
|
journalActivation(runtimeRoot, pointer.pending.action_id, "rolled-back", detail);
|
|
773
1197
|
} catch {
|
|
@@ -779,7 +1203,7 @@ function acknowledgeActivationFailure(runtimeRoot, pointer) {
|
|
|
779
1203
|
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id || current?.pending?.terminal_status !== "failed") {
|
|
780
1204
|
throw new Error("runtime rollback acknowledgement obligation changed");
|
|
781
1205
|
}
|
|
782
|
-
atomicWriteJson(
|
|
1206
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), { ...current, pending: null });
|
|
783
1207
|
try {
|
|
784
1208
|
journalActivation(runtimeRoot, pointer.pending.action_id, "failure-acknowledged", pointer.pending.terminal_detail);
|
|
785
1209
|
} catch {
|
|
@@ -828,9 +1252,9 @@ function buildMinimalMaintenanceEnv(env = process.env, runtimeRoot = "") {
|
|
|
828
1252
|
npm_config_update_notifier: "false",
|
|
829
1253
|
npm_config_registry: PUBLIC_REGISTRY,
|
|
830
1254
|
...runtimeRoot ? {
|
|
831
|
-
npm_config_userconfig:
|
|
832
|
-
npm_config_globalconfig:
|
|
833
|
-
npm_config_cache:
|
|
1255
|
+
npm_config_userconfig: join3(runtimeRoot, "maintenance", "user.npmrc"),
|
|
1256
|
+
npm_config_globalconfig: join3(runtimeRoot, "maintenance", "global.npmrc"),
|
|
1257
|
+
npm_config_cache: join3(runtimeRoot, "maintenance", "npm-cache")
|
|
834
1258
|
} : {}
|
|
835
1259
|
};
|
|
836
1260
|
}
|
|
@@ -870,20 +1294,20 @@ function parseJsonOutput(result, operation) {
|
|
|
870
1294
|
}
|
|
871
1295
|
}
|
|
872
1296
|
function tarballIntegrity(file) {
|
|
873
|
-
return `sha512-${
|
|
1297
|
+
return `sha512-${createHash4("sha512").update(readFileSync4(file)).digest("base64")}`;
|
|
874
1298
|
}
|
|
875
1299
|
function assertNoLinks(root) {
|
|
876
1300
|
const pending = [root];
|
|
877
1301
|
while (pending.length) {
|
|
878
1302
|
const current = pending.pop();
|
|
879
|
-
const stat =
|
|
1303
|
+
const stat = lstatSync3(current);
|
|
880
1304
|
if (stat.isSymbolicLink()) throw new Error("installed runtime contains a link/reparse point");
|
|
881
1305
|
if (!stat.isDirectory()) continue;
|
|
882
|
-
for (const entry of
|
|
1306
|
+
for (const entry of readdirSync3(current)) pending.push(join3(current, entry));
|
|
883
1307
|
}
|
|
884
1308
|
}
|
|
885
1309
|
function validateDependencyLock(payloadRoot, expected) {
|
|
886
|
-
const lock = JSON.parse(
|
|
1310
|
+
const lock = JSON.parse(readFileSync4(join3(payloadRoot, "package-lock.json"), "utf8"));
|
|
887
1311
|
if (Number(lock.lockfileVersion) < 3 || !lock.packages || typeof lock.packages !== "object") {
|
|
888
1312
|
throw new Error("runtime dependency lock is missing or unsupported");
|
|
889
1313
|
}
|
|
@@ -913,23 +1337,40 @@ function buildActive(slotId, metadata, paths) {
|
|
|
913
1337
|
tree_sha512: hashRuntimeTree(paths.slotRoot ?? paths.payloadRoot)
|
|
914
1338
|
};
|
|
915
1339
|
}
|
|
916
|
-
function installSlot({
|
|
917
|
-
|
|
1340
|
+
function installSlot({
|
|
1341
|
+
runtimeRoot,
|
|
1342
|
+
metadata,
|
|
1343
|
+
tarball,
|
|
1344
|
+
runner,
|
|
1345
|
+
npmEnv,
|
|
1346
|
+
runOptions,
|
|
1347
|
+
force,
|
|
1348
|
+
runtimeAuthorization
|
|
1349
|
+
}) {
|
|
1350
|
+
const digest = createHash4("sha256").update(metadata.integrity).digest("hex").slice(0, 16);
|
|
918
1351
|
const suffix = force ? `${digest}-${randomUUID2().slice(0, 8)}` : digest;
|
|
919
1352
|
const slotId = `vo-mcp-${metadata.version}-${suffix}`;
|
|
920
1353
|
const finalPaths = slotPaths(runtimeRoot, slotId);
|
|
921
|
-
if (!force &&
|
|
922
|
-
const manifest = JSON.parse(
|
|
1354
|
+
if (!force && existsSync4(finalPaths.slotRoot)) {
|
|
1355
|
+
const manifest = JSON.parse(readFileSync4(finalPaths.manifest, "utf8"));
|
|
923
1356
|
const active = buildActive(slotId, metadata, finalPaths);
|
|
924
1357
|
const validated = validateSlot(runtimeRoot, active);
|
|
925
|
-
if (validated.ok && manifest.integrity === metadata.integrity)
|
|
1358
|
+
if (validated.ok && manifest.integrity === metadata.integrity) {
|
|
1359
|
+
if (runtimeAuthorization) {
|
|
1360
|
+
validateStagedRuntimeAuthorization({
|
|
1361
|
+
payloadRoot: finalPaths.slotRoot,
|
|
1362
|
+
authorization: runtimeAuthorization
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
return { active, created: false };
|
|
1366
|
+
}
|
|
926
1367
|
}
|
|
927
|
-
const staging =
|
|
928
|
-
const payload =
|
|
1368
|
+
const staging = join3(runtimeRoot, "staging", randomUUID2());
|
|
1369
|
+
const payload = join3(staging, "payload");
|
|
929
1370
|
let installedSlot = false;
|
|
930
1371
|
try {
|
|
931
1372
|
mkdirSync2(payload, { recursive: true });
|
|
932
|
-
writeFileSync2(
|
|
1373
|
+
writeFileSync2(join3(payload, "package.json"), `${JSON.stringify({ name: "algohq-runner-runtime", version: "0.0.0", private: true })}
|
|
933
1374
|
`);
|
|
934
1375
|
const install = runner.npm([
|
|
935
1376
|
"install",
|
|
@@ -945,24 +1386,27 @@ function installSlot({ runtimeRoot, metadata, tarball, runner, npmEnv, runOption
|
|
|
945
1386
|
if (install.status !== 0) throw new Error(`npm install failed: ${install.stderr || install.error?.message || install.status}`);
|
|
946
1387
|
assertNoLinks(payload);
|
|
947
1388
|
validateDependencyLock(payload, metadata);
|
|
1389
|
+
if (runtimeAuthorization) {
|
|
1390
|
+
validateStagedRuntimeAuthorization({ payloadRoot: payload, authorization: runtimeAuthorization });
|
|
1391
|
+
}
|
|
948
1392
|
const stagedPaths = {
|
|
949
|
-
entry:
|
|
950
|
-
supervisor:
|
|
951
|
-
packageJson:
|
|
952
|
-
credentialHelper:
|
|
1393
|
+
entry: join3(payload, "node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp"),
|
|
1394
|
+
supervisor: join3(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js"),
|
|
1395
|
+
packageJson: join3(payload, "node_modules", "@algosuite", "vo-mcp", "package.json"),
|
|
1396
|
+
credentialHelper: join3(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js"),
|
|
953
1397
|
slotRoot: payload
|
|
954
1398
|
};
|
|
955
|
-
const pkg = JSON.parse(
|
|
1399
|
+
const pkg = JSON.parse(readFileSync4(stagedPaths.packageJson, "utf8"));
|
|
956
1400
|
if (pkg.name !== PACKAGE_NAME || pkg.version !== metadata.version) throw new Error("installed package identity mismatch");
|
|
957
|
-
if (!
|
|
1401
|
+
if (!lstatSync3(stagedPaths.credentialHelper).isFile()) throw new Error("installed credential helper is missing");
|
|
958
1402
|
const smoke = runner.node([stagedPaths.entry, "runner", "--version"], { ...runOptions, cwd: payload, env: npmEnv, timeout: 3e4 });
|
|
959
1403
|
if (smoke.status !== 0 || String(smoke.stdout || "").trim() !== `vo-mcp runner ${metadata.version}`) {
|
|
960
1404
|
throw new Error("bundled runtime smoke check failed");
|
|
961
1405
|
}
|
|
962
1406
|
const active = buildActive(slotId, metadata, stagedPaths);
|
|
963
|
-
atomicWriteJson(
|
|
964
|
-
mkdirSync2(
|
|
965
|
-
if (
|
|
1407
|
+
atomicWriteJson(join3(payload, "runtime-manifest.json"), { schema_version: 1, ...active });
|
|
1408
|
+
mkdirSync2(join3(runtimeRoot, "slots"), { recursive: true });
|
|
1409
|
+
if (existsSync4(finalPaths.slotRoot)) throw new Error("immutable runtime slot already exists");
|
|
966
1410
|
renameSync2(payload, finalPaths.slotRoot);
|
|
967
1411
|
installedSlot = true;
|
|
968
1412
|
const validated = validateSlot(runtimeRoot, active);
|
|
@@ -984,11 +1428,12 @@ function stageBundledRuntimeSlot(options) {
|
|
|
984
1428
|
platform = process.platform,
|
|
985
1429
|
execPath = process.execPath,
|
|
986
1430
|
env = process.env,
|
|
987
|
-
fileExists =
|
|
1431
|
+
fileExists = existsSync4,
|
|
988
1432
|
run = defaultRun,
|
|
989
|
-
force = false
|
|
1433
|
+
force = false,
|
|
1434
|
+
runtimeAuthorization = null
|
|
990
1435
|
} = options;
|
|
991
|
-
if (!runtimeRoot || !
|
|
1436
|
+
if (!runtimeRoot || !isAbsolute3(runtimeRoot)) return { ok: false, status: 2, detail: "bundled runtime root unavailable" };
|
|
992
1437
|
if (!expectedVersion || !PACKAGE_SPEC_RE2.test(`${PACKAGE_NAME}@${expectedVersion}`)) {
|
|
993
1438
|
return { ok: false, status: 2, detail: "invalid expected runner version" };
|
|
994
1439
|
}
|
|
@@ -997,18 +1442,18 @@ function stageBundledRuntimeSlot(options) {
|
|
|
997
1442
|
}
|
|
998
1443
|
const exactSpec = `${PACKAGE_NAME}@${expectedVersion}`;
|
|
999
1444
|
if (packageSpec !== exactSpec) return { ok: false, status: 2, detail: "runner package spec does not match authorized version" };
|
|
1000
|
-
const resolvedRoot =
|
|
1445
|
+
const resolvedRoot = resolve4(runtimeRoot);
|
|
1001
1446
|
const npmEnv = buildMinimalMaintenanceEnv(env, resolvedRoot);
|
|
1002
1447
|
const runOptions = { env: npmEnv, cwd: resolvedRoot };
|
|
1003
1448
|
let tarDir = null;
|
|
1004
1449
|
try {
|
|
1005
1450
|
mkdirSync2(resolvedRoot, { recursive: true });
|
|
1006
|
-
mkdirSync2(
|
|
1451
|
+
mkdirSync2(join3(resolvedRoot, "maintenance"), { recursive: true });
|
|
1007
1452
|
writeFileSync2(npmEnv.npm_config_userconfig, "", { mode: 384 });
|
|
1008
1453
|
writeFileSync2(npmEnv.npm_config_globalconfig, "", { mode: 384 });
|
|
1009
1454
|
const runner = commandRunner({ platform, execPath, env: npmEnv, fileExists, run });
|
|
1010
1455
|
const metadata = { version: expectedVersion, integrity: expectedIntegrity };
|
|
1011
|
-
tarDir =
|
|
1456
|
+
tarDir = join3(resolvedRoot, "staging", randomUUID2());
|
|
1012
1457
|
mkdirSync2(tarDir, { recursive: true });
|
|
1013
1458
|
const packed = parseJsonOutput(runner.npm([
|
|
1014
1459
|
"pack",
|
|
@@ -1020,13 +1465,22 @@ function stageBundledRuntimeSlot(options) {
|
|
|
1020
1465
|
`--registry=${PUBLIC_REGISTRY}`
|
|
1021
1466
|
], runOptions), "npm pack");
|
|
1022
1467
|
const record = Array.isArray(packed) ? packed[0] : packed;
|
|
1023
|
-
const tarball =
|
|
1024
|
-
if (!
|
|
1025
|
-
if (
|
|
1468
|
+
const tarball = join3(tarDir, basename(String(record?.filename || "")));
|
|
1469
|
+
if (!existsSync4(tarball) || !basename(tarball).endsWith(".tgz")) throw new Error("npm pack returned no tarball");
|
|
1470
|
+
if (lstatSync3(tarball).size > MAX_TARBALL_BYTES) throw new Error("runner package tarball exceeds size limit");
|
|
1026
1471
|
if (record.integrity !== metadata.integrity || tarballIntegrity(tarball) !== metadata.integrity) {
|
|
1027
1472
|
throw new Error("runner package sha512 integrity mismatch");
|
|
1028
1473
|
}
|
|
1029
|
-
const installed = installSlot({
|
|
1474
|
+
const installed = installSlot({
|
|
1475
|
+
runtimeRoot: resolvedRoot,
|
|
1476
|
+
metadata,
|
|
1477
|
+
tarball,
|
|
1478
|
+
runner,
|
|
1479
|
+
npmEnv,
|
|
1480
|
+
runOptions,
|
|
1481
|
+
force,
|
|
1482
|
+
runtimeAuthorization
|
|
1483
|
+
});
|
|
1030
1484
|
return { ok: true, status: 0, ...installed };
|
|
1031
1485
|
} catch (error) {
|
|
1032
1486
|
return { ok: false, status: 1, detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env) };
|
|
@@ -1038,7 +1492,7 @@ function stageAndActivateBundledUpdate(options) {
|
|
|
1038
1492
|
const staged = stageBundledRuntimeSlot(options);
|
|
1039
1493
|
if (!staged.ok) return staged;
|
|
1040
1494
|
try {
|
|
1041
|
-
activateSlot(
|
|
1495
|
+
activateSlot(resolve4(options.runtimeRoot), staged.active, options.action);
|
|
1042
1496
|
return { ...staged, handoff: true };
|
|
1043
1497
|
} catch (error) {
|
|
1044
1498
|
return {
|
|
@@ -1051,7 +1505,7 @@ function stageAndActivateBundledUpdate(options) {
|
|
|
1051
1505
|
|
|
1052
1506
|
// src/runner/supervisor-activation.mjs
|
|
1053
1507
|
var MAX_ACK_ATTEMPTS = 3;
|
|
1054
|
-
var delay = (ms) => new Promise((
|
|
1508
|
+
var delay = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1055
1509
|
async function waitForAuthoritativeRunnerHeartbeat({
|
|
1056
1510
|
client,
|
|
1057
1511
|
runnerId: runnerId2,
|
|
@@ -1405,14 +1859,14 @@ async function prepareSupervisorAuth({
|
|
|
1405
1859
|
|
|
1406
1860
|
// src/runner/supervisor-credential-reader.mjs
|
|
1407
1861
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1408
|
-
import { existsSync as
|
|
1409
|
-
import { dirname as
|
|
1410
|
-
import { fileURLToPath } from "node:url";
|
|
1862
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1863
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1864
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1411
1865
|
function defaultCredentialHelperPath(metaUrl = import.meta.url) {
|
|
1412
|
-
const moduleDir =
|
|
1413
|
-
const bundled =
|
|
1414
|
-
const source =
|
|
1415
|
-
return
|
|
1866
|
+
const moduleDir = dirname3(fileURLToPath2(metaUrl));
|
|
1867
|
+
const bundled = join4(moduleDir, "supervisor-credential-helper.js");
|
|
1868
|
+
const source = join4(moduleDir, "..", "supervisor-credential-helper.mjs");
|
|
1869
|
+
return existsSync5(source) ? source : bundled;
|
|
1416
1870
|
}
|
|
1417
1871
|
function readStoredCredentialIsolated({
|
|
1418
1872
|
spawn: spawn2 = spawnSync3,
|
|
@@ -1446,9 +1900,9 @@ var POLL_MS = 5e3;
|
|
|
1446
1900
|
var CHILD_START_MS = 1500;
|
|
1447
1901
|
var SUPERVISOR_CAPABILITIES = ["bundled-runtime-slots-v1"];
|
|
1448
1902
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
1449
|
-
var selfPath =
|
|
1450
|
-
var childEntry =
|
|
1451
|
-
var sleep = (ms) => new Promise((
|
|
1903
|
+
var selfPath = fileURLToPath3(import.meta.url);
|
|
1904
|
+
var childEntry = join5(dirname4(selfPath), "runner-cli.js");
|
|
1905
|
+
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1452
1906
|
var runnerId = resolveSupervisorRunnerId(process.env);
|
|
1453
1907
|
function packageVersion() {
|
|
1454
1908
|
try {
|
|
@@ -1507,7 +1961,7 @@ async function stopChild(child) {
|
|
|
1507
1961
|
if (!child || child.exitCode !== null) return;
|
|
1508
1962
|
child.kill(process.platform === "win32" ? void 0 : "SIGTERM");
|
|
1509
1963
|
await Promise.race([
|
|
1510
|
-
new Promise((
|
|
1964
|
+
new Promise((resolve5) => child.once("exit", resolve5)),
|
|
1511
1965
|
sleep(15e3)
|
|
1512
1966
|
]);
|
|
1513
1967
|
if (child.exitCode === null) child.kill("SIGKILL");
|