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