@algosuite/vo-mcp 0.2.0-beta.10 → 0.2.0-beta.12
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 +177 -19
- package/dist/runner-cli.js.map +3 -3
- package/dist/runner-supervisor.js +945 -32
- package/dist/runner-supervisor.js.map +4 -4
- package/dist/update-cli.js +75 -12
- package/dist/update-cli.js.map +4 -4
- package/package.json +1 -1
|
@@ -31,9 +31,10 @@ var init_control_plane_auth_stub = __esm({
|
|
|
31
31
|
|
|
32
32
|
// src/runner-supervisor.mjs
|
|
33
33
|
import { spawn } from "node:child_process";
|
|
34
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
34
35
|
import { createRequire } from "node:module";
|
|
35
36
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
36
|
-
import { dirname as
|
|
37
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
37
38
|
|
|
38
39
|
// ../../scripts/virtual-office/code-runner/control-plane-client.mjs
|
|
39
40
|
var cachedFirebaseToken = null;
|
|
@@ -54,22 +55,41 @@ async function resolveBearer(env) {
|
|
|
54
55
|
function createControlPlaneClient({
|
|
55
56
|
baseUrl = process.env.VO_CONTROL_PLANE_URL || "",
|
|
56
57
|
env = process.env,
|
|
57
|
-
fetchImpl = fetch
|
|
58
|
+
fetchImpl = fetch,
|
|
59
|
+
heartbeatTimeoutMs = Math.min(
|
|
60
|
+
Math.max(Number(env.VO_CODE_RUNNER_HEARTBEAT_TIMEOUT_MS) || 15e3, 1e3),
|
|
61
|
+
6e4
|
|
62
|
+
)
|
|
58
63
|
} = {}) {
|
|
59
64
|
if (!baseUrl) {
|
|
60
65
|
throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
|
|
61
66
|
}
|
|
62
67
|
const root = baseUrl.replace(/\/+$/, "");
|
|
63
|
-
async function req(method, path, body) {
|
|
68
|
+
async function req(method, path, body, { timeoutMs } = {}) {
|
|
64
69
|
const bearer = await resolveBearer(env);
|
|
65
|
-
|
|
70
|
+
const controller = timeoutMs ? new AbortController() : null;
|
|
71
|
+
let timeoutId;
|
|
72
|
+
const request = Promise.resolve(fetchImpl(`${root}${path}`, {
|
|
66
73
|
method,
|
|
67
74
|
headers: {
|
|
68
75
|
"content-type": "application/json",
|
|
69
76
|
authorization: `Bearer ${bearer}`
|
|
70
77
|
},
|
|
71
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
78
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
79
|
+
...controller ? { signal: controller.signal } : {}
|
|
80
|
+
}));
|
|
81
|
+
if (!timeoutMs) return request;
|
|
82
|
+
const timeout = new Promise((_, reject) => {
|
|
83
|
+
timeoutId = setTimeout(() => {
|
|
84
|
+
controller.abort();
|
|
85
|
+
reject(new Error(`control-plane ${path} timed out after ${timeoutMs}ms`));
|
|
86
|
+
}, timeoutMs);
|
|
72
87
|
});
|
|
88
|
+
try {
|
|
89
|
+
return await Promise.race([request, timeout]);
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
94
|
return {
|
|
75
95
|
/**
|
|
@@ -85,6 +105,10 @@ function createControlPlaneClient({
|
|
|
85
105
|
if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
|
|
86
106
|
if (session.runnerInstanceId) body.runner_instance_id = session.runnerInstanceId;
|
|
87
107
|
if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
|
|
108
|
+
if (session.defaultAgent) body.default_agent = session.defaultAgent;
|
|
109
|
+
if (Array.isArray(session.availableAgents)) {
|
|
110
|
+
body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
|
|
111
|
+
}
|
|
88
112
|
const res = await req("POST", "/api/v1/code-task/claim", body);
|
|
89
113
|
if (res.status === 401) {
|
|
90
114
|
cachedFirebaseToken = null;
|
|
@@ -228,13 +252,20 @@ function createControlPlaneClient({
|
|
|
228
252
|
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
229
253
|
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
230
254
|
*/
|
|
231
|
-
async postHeartbeat({ runnerId: runnerId2, operatorId, uptimeSec, activeTasks, version, daemonVersion, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
255
|
+
async postHeartbeat({ runnerId: runnerId2, runnerInstanceId, operatorId, uptimeSec, activeTasks, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
232
256
|
const body = { runner_id: runnerId2 };
|
|
257
|
+
if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
|
|
233
258
|
if (operatorId) body.operator_id = operatorId;
|
|
234
259
|
if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
|
|
235
260
|
if (typeof activeTasks === "number") body.active_tasks = activeTasks;
|
|
236
261
|
if (version) body.version = version;
|
|
237
262
|
if (daemonVersion) body.daemon_version = daemonVersion;
|
|
263
|
+
if (defaultAgent) body.default_agent = defaultAgent;
|
|
264
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
265
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
266
|
+
if (Array.isArray(supervisorCapabilities) && supervisorCapabilities.length > 0) {
|
|
267
|
+
body.supervisor_capabilities = supervisorCapabilities;
|
|
268
|
+
}
|
|
238
269
|
if (Array.isArray(servedRepos) && servedRepos.length > 0) body.served_repos = servedRepos;
|
|
239
270
|
if (Array.isArray(servedOperators) && servedOperators.length > 0) {
|
|
240
271
|
body.served_operator_ids = servedOperators;
|
|
@@ -245,7 +276,9 @@ function createControlPlaneClient({
|
|
|
245
276
|
if (Array.isArray(accountUsage) && accountUsage.length > 0) {
|
|
246
277
|
body.account_usage = accountUsage;
|
|
247
278
|
}
|
|
248
|
-
const res = await req("POST", "/api/v1/runner/heartbeat", body
|
|
279
|
+
const res = await req("POST", "/api/v1/runner/heartbeat", body, {
|
|
280
|
+
timeoutMs: heartbeatTimeoutMs
|
|
281
|
+
});
|
|
249
282
|
if (res.status === 401) {
|
|
250
283
|
cachedFirebaseToken = null;
|
|
251
284
|
throw new Error("heartbeat unauthorized (401)");
|
|
@@ -253,10 +286,27 @@ function createControlPlaneClient({
|
|
|
253
286
|
if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
|
|
254
287
|
return true;
|
|
255
288
|
},
|
|
289
|
+
/** Read the server-authoritative heartbeat ledger without mutating it. */
|
|
290
|
+
async getRunnerStatus({ operatorId } = {}) {
|
|
291
|
+
const query = operatorId ? `?operator_id=${encodeURIComponent(operatorId)}` : "";
|
|
292
|
+
const res = await req("GET", `/api/v1/runner/status${query}`, void 0, {
|
|
293
|
+
timeoutMs: heartbeatTimeoutMs
|
|
294
|
+
});
|
|
295
|
+
if (res.status === 401) {
|
|
296
|
+
cachedFirebaseToken = null;
|
|
297
|
+
throw new Error("runner status unauthorized (401)");
|
|
298
|
+
}
|
|
299
|
+
if (!res.ok) throw new Error(`runner status failed: HTTP ${res.status}`);
|
|
300
|
+
const body = await res.json();
|
|
301
|
+
return Array.isArray(body?.runners) ? body.runners : [];
|
|
302
|
+
},
|
|
256
303
|
/** Poll one authenticated runner's durable Mission Control action queue. */
|
|
257
|
-
async pollRunnerControl({ runnerId: runnerId2, operatorId }) {
|
|
304
|
+
async pollRunnerControl({ runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities }) {
|
|
258
305
|
const body = { runner_id: runnerId2 };
|
|
259
306
|
if (operatorId) body.operator_id = operatorId;
|
|
307
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
308
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
309
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
260
310
|
const res = await req("POST", "/api/v1/runner/control/poll", body);
|
|
261
311
|
if (res.status === 401) {
|
|
262
312
|
cachedFirebaseToken = null;
|
|
@@ -268,9 +318,12 @@ function createControlPlaneClient({
|
|
|
268
318
|
return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
|
|
269
319
|
},
|
|
270
320
|
/** Acknowledge a maintenance action after the host has restarted the child. */
|
|
271
|
-
async completeRunnerControl(actionId, { runnerId: runnerId2, operatorId, status, detail }) {
|
|
321
|
+
async completeRunnerControl(actionId, { runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities, status, detail }) {
|
|
272
322
|
const body = { runner_id: runnerId2, status };
|
|
273
323
|
if (operatorId) body.operator_id = operatorId;
|
|
324
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
325
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
326
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
274
327
|
if (detail) body.detail = detail;
|
|
275
328
|
const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
|
|
276
329
|
if (res.status === 401) {
|
|
@@ -333,9 +386,12 @@ function createControlPlaneClient({
|
|
|
333
386
|
|
|
334
387
|
// ../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs
|
|
335
388
|
import { spawnSync } from "node:child_process";
|
|
389
|
+
import { existsSync } from "node:fs";
|
|
390
|
+
import { win32 } from "node:path";
|
|
336
391
|
var DEFAULT_RUNNER_PACKAGE = "@algosuite/vo-mcp@beta";
|
|
337
392
|
var PACKAGE_SPEC_RE = /^@algosuite\/vo-mcp@(beta|latest|\d+(?:\.\d+){0,2}(?:-[\w.-]+)?)$/u;
|
|
338
393
|
var MAX_DIAGNOSTIC_CHARS = 800;
|
|
394
|
+
var NPM_CLI_SUFFIX = `\\${win32.join("node_modules", "npm", "bin", "npm-cli.js").toLowerCase()}`;
|
|
339
395
|
function escapeRegExp(value) {
|
|
340
396
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
341
397
|
}
|
|
@@ -351,11 +407,46 @@ function sanitizeMaintenanceDiagnostic(raw, env = {}) {
|
|
|
351
407
|
if (value.length <= MAX_DIAGNOSTIC_CHARS) return value;
|
|
352
408
|
return `${value.slice(0, MAX_DIAGNOSTIC_CHARS - 1)}\u2026`;
|
|
353
409
|
}
|
|
354
|
-
function
|
|
410
|
+
function isNpmCliPath(value) {
|
|
411
|
+
return typeof value === "string" && win32.isAbsolute(value) && win32.normalize(value).toLowerCase().endsWith(NPM_CLI_SUFFIX);
|
|
412
|
+
}
|
|
413
|
+
function resolveNpmCli({
|
|
414
|
+
env = process.env,
|
|
415
|
+
execPath = process.execPath,
|
|
416
|
+
fileExists = existsSync
|
|
417
|
+
} = {}) {
|
|
418
|
+
const candidates = [];
|
|
419
|
+
if (isNpmCliPath(env.npm_execpath)) candidates.push(env.npm_execpath);
|
|
420
|
+
candidates.push(win32.join(win32.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"));
|
|
421
|
+
const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
|
|
422
|
+
for (const entry of pathValue.split(";")) {
|
|
423
|
+
const trimmed = entry.trim();
|
|
424
|
+
if (!win32.isAbsolute(trimmed)) continue;
|
|
425
|
+
candidates.push(win32.join(trimmed, "node_modules", "npm", "bin", "npm-cli.js"));
|
|
426
|
+
}
|
|
427
|
+
const seen = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const candidate of candidates) {
|
|
429
|
+
const normalized = win32.normalize(candidate);
|
|
430
|
+
const key = normalized.toLowerCase();
|
|
431
|
+
if (seen.has(key) || !isNpmCliPath(normalized)) continue;
|
|
432
|
+
seen.add(key);
|
|
433
|
+
if (fileExists(normalized)) return normalized;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function buildMaintenanceCommand(kind, {
|
|
438
|
+
platform = process.platform,
|
|
439
|
+
packageSpec = DEFAULT_RUNNER_PACKAGE,
|
|
440
|
+
env = process.env,
|
|
441
|
+
execPath = process.execPath,
|
|
442
|
+
fileExists = existsSync
|
|
443
|
+
} = {}) {
|
|
355
444
|
if (!["update", "reinstall"].includes(kind)) return null;
|
|
356
445
|
if (!PACKAGE_SPEC_RE.test(packageSpec)) throw new Error("unsafe runner package spec");
|
|
357
|
-
const
|
|
358
|
-
|
|
446
|
+
const npmCli = platform === "win32" ? resolveNpmCli({ env, execPath, fileExists }) : null;
|
|
447
|
+
if (platform === "win32" && !npmCli) throw new Error("trusted npm CLI not found");
|
|
448
|
+
const command = platform === "win32" ? execPath : "npm";
|
|
449
|
+
const args = [...npmCli ? [npmCli] : [], "install", "-g", packageSpec];
|
|
359
450
|
if (kind === "reinstall") args.push("--force");
|
|
360
451
|
return { command, args };
|
|
361
452
|
}
|
|
@@ -363,12 +454,25 @@ function runHostMaintenance(kind, {
|
|
|
363
454
|
platform = process.platform,
|
|
364
455
|
packageSpec = DEFAULT_RUNNER_PACKAGE,
|
|
365
456
|
env = process.env,
|
|
457
|
+
execPath = process.execPath,
|
|
458
|
+
fileExists = existsSync,
|
|
366
459
|
spawn: spawn2 = spawnSync,
|
|
367
460
|
log = () => {
|
|
368
461
|
}
|
|
369
462
|
} = {}) {
|
|
370
463
|
if (kind === "reconnect") return { ok: true, status: 0, command: null, args: [] };
|
|
371
|
-
|
|
464
|
+
let command;
|
|
465
|
+
try {
|
|
466
|
+
command = buildMaintenanceCommand(kind, { platform, packageSpec, env, execPath, fileExists });
|
|
467
|
+
} catch (error) {
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
status: 2,
|
|
471
|
+
command: null,
|
|
472
|
+
args: [],
|
|
473
|
+
detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
372
476
|
if (!command) return { ok: false, status: 2, command: null, args: [] };
|
|
373
477
|
log(`runner maintenance: ${command.command} ${command.args.join(" ")}`);
|
|
374
478
|
const result = spawn2(command.command, command.args, {
|
|
@@ -387,6 +491,739 @@ function runHostMaintenance(kind, {
|
|
|
387
491
|
return { ok: status === 0, status, command: command.command, args: command.args, detail };
|
|
388
492
|
}
|
|
389
493
|
|
|
494
|
+
// src/runner/bundled-runtime-updater.mjs
|
|
495
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
496
|
+
import {
|
|
497
|
+
existsSync as existsSync3,
|
|
498
|
+
lstatSync as lstatSync2,
|
|
499
|
+
mkdirSync as mkdirSync2,
|
|
500
|
+
readFileSync as readFileSync2,
|
|
501
|
+
readdirSync as readdirSync2,
|
|
502
|
+
renameSync as renameSync2,
|
|
503
|
+
rmSync as rmSync2,
|
|
504
|
+
writeFileSync as writeFileSync2
|
|
505
|
+
} from "node:fs";
|
|
506
|
+
import { basename, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
|
|
507
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
508
|
+
|
|
509
|
+
// src/runner/bundled-runtime-store.mjs
|
|
510
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
511
|
+
import {
|
|
512
|
+
closeSync,
|
|
513
|
+
existsSync as existsSync2,
|
|
514
|
+
fsyncSync,
|
|
515
|
+
lstatSync,
|
|
516
|
+
mkdirSync,
|
|
517
|
+
openSync,
|
|
518
|
+
readFileSync,
|
|
519
|
+
readdirSync,
|
|
520
|
+
realpathSync,
|
|
521
|
+
renameSync,
|
|
522
|
+
rmSync,
|
|
523
|
+
writeFileSync
|
|
524
|
+
} from "node:fs";
|
|
525
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
526
|
+
var SLOT_ID_RE = /^vo-mcp-[0-9A-Za-z._-]{1,96}$/u;
|
|
527
|
+
var ACTION_ID_RE = /^[0-9A-Za-z._-]{1,128}$/u;
|
|
528
|
+
var INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
529
|
+
var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
530
|
+
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 = join("node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp");
|
|
532
|
+
var SUPERVISOR_REL = join("node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js");
|
|
533
|
+
var PACKAGE_REL = join("node_modules", "@algosuite", "vo-mcp", "package.json");
|
|
534
|
+
var CREDENTIAL_HELPER_REL = join("node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js");
|
|
535
|
+
var MANIFEST_FILE = "runtime-manifest.json";
|
|
536
|
+
function within(parent, candidate) {
|
|
537
|
+
const rel = relative(resolve(parent), resolve(candidate));
|
|
538
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
539
|
+
}
|
|
540
|
+
function runtimeRootFromEnv(env = process.env) {
|
|
541
|
+
const value = String(env.VO_RUNNER_RUNTIME_ROOT || "").trim();
|
|
542
|
+
return value && isAbsolute(value) ? resolve(value) : null;
|
|
543
|
+
}
|
|
544
|
+
function hashFileSha512(file) {
|
|
545
|
+
return `sha512-${createHash("sha512").update(readFileSync(file)).digest("base64")}`;
|
|
546
|
+
}
|
|
547
|
+
function hashRuntimeTree(root) {
|
|
548
|
+
const hasher = createHash("sha512");
|
|
549
|
+
const files = [];
|
|
550
|
+
const visit = (directory, prefix = "") => {
|
|
551
|
+
const rootStat = lstatSync(directory);
|
|
552
|
+
if (rootStat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
553
|
+
if (!rootStat.isDirectory()) throw new Error("runtime tree root is not a directory");
|
|
554
|
+
for (const name of readdirSync(directory).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) {
|
|
555
|
+
const absolute = join(directory, name);
|
|
556
|
+
const relativePath = prefix ? `${prefix}/${name}` : name;
|
|
557
|
+
const stat = lstatSync(absolute);
|
|
558
|
+
if (stat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
559
|
+
if (stat.isDirectory()) visit(absolute, relativePath);
|
|
560
|
+
else if (stat.isFile() && relativePath !== MANIFEST_FILE) files.push({ absolute, relativePath, size: stat.size });
|
|
561
|
+
else if (!stat.isFile()) throw new Error("runtime tree contains a non-regular file");
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
visit(root);
|
|
565
|
+
files.sort((a, b) => Buffer.compare(Buffer.from(a.relativePath), Buffer.from(b.relativePath)));
|
|
566
|
+
for (const file of files) {
|
|
567
|
+
const pathBytes = Buffer.from(file.relativePath, "utf8");
|
|
568
|
+
hasher.update(`${pathBytes.length}:`);
|
|
569
|
+
hasher.update(pathBytes);
|
|
570
|
+
hasher.update(`:${file.size}:`);
|
|
571
|
+
hasher.update(readFileSync(file.absolute));
|
|
572
|
+
hasher.update("\n");
|
|
573
|
+
}
|
|
574
|
+
return `sha512-${hasher.digest("base64")}`;
|
|
575
|
+
}
|
|
576
|
+
function atomicWriteJson(file, value) {
|
|
577
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
578
|
+
const temp = join(dirname(file), `.${randomUUID()}.tmp`);
|
|
579
|
+
const fd = openSync(temp, "wx", 384);
|
|
580
|
+
try {
|
|
581
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}
|
|
582
|
+
`, "utf8");
|
|
583
|
+
fsyncSync(fd);
|
|
584
|
+
} finally {
|
|
585
|
+
closeSync(fd);
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
renameSync(temp, file);
|
|
589
|
+
if (process.platform !== "win32") {
|
|
590
|
+
try {
|
|
591
|
+
const parentFd = openSync(dirname(file), "r");
|
|
592
|
+
try {
|
|
593
|
+
fsyncSync(parentFd);
|
|
594
|
+
} finally {
|
|
595
|
+
closeSync(parentFd);
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
rmSync(temp, { force: true });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function readActivation(runtimeRoot) {
|
|
605
|
+
const file = join(runtimeRoot, "current.json");
|
|
606
|
+
if (!existsSync2(file)) return null;
|
|
607
|
+
try {
|
|
608
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
609
|
+
return value?.schema_version === 1 ? value : null;
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function slotPaths(runtimeRoot, slotId) {
|
|
615
|
+
if (!SLOT_ID_RE.test(slotId)) throw new Error("invalid runtime slot id");
|
|
616
|
+
const slotRoot = join(runtimeRoot, "slots", slotId);
|
|
617
|
+
return {
|
|
618
|
+
slotRoot,
|
|
619
|
+
entry: join(slotRoot, ENTRY_REL),
|
|
620
|
+
supervisor: join(slotRoot, SUPERVISOR_REL),
|
|
621
|
+
packageJson: join(slotRoot, PACKAGE_REL),
|
|
622
|
+
credentialHelper: join(slotRoot, CREDENTIAL_HELPER_REL),
|
|
623
|
+
manifest: join(slotRoot, MANIFEST_FILE)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function validActive(active) {
|
|
627
|
+
return active && SLOT_ID_RE.test(active.slot_id) && VERSION_RE.test(active.version) && INTEGRITY_RE.test(active.integrity) && INTEGRITY_RE.test(active.entry_sha512) && INTEGRITY_RE.test(active.supervisor_sha512) && INTEGRITY_RE.test(active.tree_sha512);
|
|
628
|
+
}
|
|
629
|
+
function validateSlot(runtimeRoot, active) {
|
|
630
|
+
if (!validActive(active)) return { ok: false, detail: "invalid activation metadata" };
|
|
631
|
+
const paths = slotPaths(runtimeRoot, active.slot_id);
|
|
632
|
+
try {
|
|
633
|
+
const manifest = JSON.parse(readFileSync(paths.manifest, "utf8"));
|
|
634
|
+
const pkg = JSON.parse(readFileSync(paths.packageJson, "utf8"));
|
|
635
|
+
const expected = {
|
|
636
|
+
slot_id: active.slot_id,
|
|
637
|
+
version: active.version,
|
|
638
|
+
integrity: active.integrity,
|
|
639
|
+
entry_sha512: active.entry_sha512,
|
|
640
|
+
supervisor_sha512: active.supervisor_sha512,
|
|
641
|
+
tree_sha512: active.tree_sha512
|
|
642
|
+
};
|
|
643
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
644
|
+
if (manifest?.[key] !== value) return { ok: false, detail: `manifest ${key} mismatch` };
|
|
645
|
+
}
|
|
646
|
+
if (manifest?.schema_version !== 1 || pkg?.name !== "@algosuite/vo-mcp" || pkg?.version !== active.version) {
|
|
647
|
+
return { ok: false, detail: "package identity mismatch" };
|
|
648
|
+
}
|
|
649
|
+
if (lstatSync(paths.entry).isSymbolicLink() || lstatSync(paths.supervisor).isSymbolicLink() || lstatSync(paths.credentialHelper).isSymbolicLink()) {
|
|
650
|
+
return { ok: false, detail: "runtime entry cannot be a link" };
|
|
651
|
+
}
|
|
652
|
+
if (!within(paths.slotRoot, realpathSync(paths.entry)) || !within(paths.slotRoot, realpathSync(paths.supervisor)) || !within(paths.slotRoot, realpathSync(paths.credentialHelper))) {
|
|
653
|
+
return { ok: false, detail: "runtime entry escaped its slot" };
|
|
654
|
+
}
|
|
655
|
+
if (hashFileSha512(paths.entry) !== active.entry_sha512) return { ok: false, detail: "entry hash mismatch" };
|
|
656
|
+
if (hashFileSha512(paths.supervisor) !== active.supervisor_sha512) return { ok: false, detail: "supervisor hash mismatch" };
|
|
657
|
+
if (hashRuntimeTree(paths.slotRoot) !== active.tree_sha512) return { ok: false, detail: "runtime tree hash mismatch" };
|
|
658
|
+
return { ok: true, paths, manifest };
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function journalActivation(runtimeRoot, actionId, state, detail = "") {
|
|
664
|
+
if (!ACTION_ID_RE.test(actionId)) throw new Error("invalid runner action id");
|
|
665
|
+
atomicWriteJson(join(runtimeRoot, "transactions", `${actionId}.json`), {
|
|
666
|
+
schema_version: 1,
|
|
667
|
+
action_id: actionId,
|
|
668
|
+
state,
|
|
669
|
+
detail: String(detail).slice(0, 400),
|
|
670
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function activateSlot(runtimeRoot, active, action) {
|
|
674
|
+
if (!validActive(active)) throw new Error("cannot activate invalid runtime metadata");
|
|
675
|
+
if (!ACTION_ID_RE.test(action.actionId)) throw new Error("invalid runner action id");
|
|
676
|
+
if (!UUID_RE.test(String(action.supervisorInstanceId || ""))) throw new Error("invalid claiming supervisor instance id");
|
|
677
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
678
|
+
if (!validated.ok) throw new Error(`cannot activate invalid runtime slot: ${validated.detail}`);
|
|
679
|
+
const current = readActivation(runtimeRoot);
|
|
680
|
+
if (current?.pending) throw new Error("another runtime activation is still pending");
|
|
681
|
+
const pointer = {
|
|
682
|
+
schema_version: 1,
|
|
683
|
+
generation: randomUUID(),
|
|
684
|
+
active,
|
|
685
|
+
previous: validActive(current?.active) ? current.active : null,
|
|
686
|
+
pending: {
|
|
687
|
+
action_id: action.actionId,
|
|
688
|
+
runner_id: String(action.runnerId || ""),
|
|
689
|
+
operator_id: String(action.operatorId || ""),
|
|
690
|
+
supervisor_instance_id: action.supervisorInstanceId,
|
|
691
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
692
|
+
ack_attempts: 0
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
journalActivation(runtimeRoot, action.actionId, "prepared", `${active.version} ${active.integrity}`);
|
|
696
|
+
atomicWriteJson(join(runtimeRoot, "current.json"), pointer);
|
|
697
|
+
return pointer;
|
|
698
|
+
}
|
|
699
|
+
function activationSupervisorInstanceId(runtimeRoot, fallback) {
|
|
700
|
+
const current = runtimeRoot ? readActivation(runtimeRoot) : null;
|
|
701
|
+
const pending = String(current?.pending?.supervisor_instance_id || "");
|
|
702
|
+
if (UUID_RE.test(pending)) return pending;
|
|
703
|
+
return UUID_RE.test(String(fallback || "")) ? fallback : null;
|
|
704
|
+
}
|
|
705
|
+
function recordActivationRetry(runtimeRoot, pointer, detail) {
|
|
706
|
+
const current = readActivation(runtimeRoot);
|
|
707
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
708
|
+
throw new Error("runtime activation generation changed before retry");
|
|
709
|
+
}
|
|
710
|
+
const attempts = Math.max(0, Number(current.pending.ack_attempts || 0)) + 1;
|
|
711
|
+
const updated = {
|
|
712
|
+
...current,
|
|
713
|
+
pending: { ...current.pending, ack_attempts: attempts, last_error: String(detail).slice(0, 240) }
|
|
714
|
+
};
|
|
715
|
+
atomicWriteJson(join(runtimeRoot, "current.json"), updated);
|
|
716
|
+
journalActivation(runtimeRoot, current.pending.action_id, "ack-retry", `attempt ${attempts}: ${detail}`);
|
|
717
|
+
return updated;
|
|
718
|
+
}
|
|
719
|
+
function attestCurrentSupervisor({ runtimeRoot, selfPath: selfPath2, version }) {
|
|
720
|
+
const pointer = readActivation(runtimeRoot);
|
|
721
|
+
if (!pointer?.pending || !validActive(pointer.active)) return { ok: false, detail: "no pending activation" };
|
|
722
|
+
const validated = validateSlot(runtimeRoot, pointer.active);
|
|
723
|
+
if (!validated.ok) return validated;
|
|
724
|
+
try {
|
|
725
|
+
if (realpathSync(selfPath2) !== realpathSync(validated.paths.supervisor)) {
|
|
726
|
+
return { ok: false, detail: "running supervisor is not the activated supervisor" };
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
return { ok: false, detail: "could not resolve running supervisor path" };
|
|
730
|
+
}
|
|
731
|
+
if (version !== pointer.active.version) return { ok: false, detail: "running supervisor version mismatch" };
|
|
732
|
+
return { ok: true, pointer, active: pointer.active, paths: validated.paths };
|
|
733
|
+
}
|
|
734
|
+
function finalizeActivation(runtimeRoot, pointer) {
|
|
735
|
+
const current = readActivation(runtimeRoot);
|
|
736
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
737
|
+
throw new Error("runtime activation generation changed before finalization");
|
|
738
|
+
}
|
|
739
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "attesting", `${pointer.active.version} verified`);
|
|
740
|
+
atomicWriteJson(join(runtimeRoot, "current.json"), {
|
|
741
|
+
schema_version: 1,
|
|
742
|
+
generation: pointer.generation,
|
|
743
|
+
active: pointer.active,
|
|
744
|
+
previous: pointer.previous || null,
|
|
745
|
+
pending: null
|
|
746
|
+
});
|
|
747
|
+
try {
|
|
748
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "attested", `${pointer.active.version} active`);
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function rollbackActivation(runtimeRoot, pointer, detail) {
|
|
753
|
+
const current = readActivation(runtimeRoot);
|
|
754
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
755
|
+
throw new Error("runtime activation generation changed before rollback");
|
|
756
|
+
}
|
|
757
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "rolling-back", detail);
|
|
758
|
+
const rolledBack = {
|
|
759
|
+
schema_version: 1,
|
|
760
|
+
generation: randomUUID(),
|
|
761
|
+
active: validActive(pointer?.previous) ? pointer.previous : null,
|
|
762
|
+
previous: null,
|
|
763
|
+
pending: {
|
|
764
|
+
...pointer.pending,
|
|
765
|
+
terminal_status: "failed",
|
|
766
|
+
terminal_detail: String(detail).slice(0, 400),
|
|
767
|
+
rolled_back_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
atomicWriteJson(join(runtimeRoot, "current.json"), rolledBack);
|
|
771
|
+
try {
|
|
772
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "rolled-back", detail);
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
return rolledBack;
|
|
776
|
+
}
|
|
777
|
+
function acknowledgeActivationFailure(runtimeRoot, pointer) {
|
|
778
|
+
const current = readActivation(runtimeRoot);
|
|
779
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id || current?.pending?.terminal_status !== "failed") {
|
|
780
|
+
throw new Error("runtime rollback acknowledgement obligation changed");
|
|
781
|
+
}
|
|
782
|
+
atomicWriteJson(join(runtimeRoot, "current.json"), { ...current, pending: null });
|
|
783
|
+
try {
|
|
784
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "failure-acknowledged", pointer.pending.terminal_detail);
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/runner/bundled-runtime-updater.mjs
|
|
790
|
+
var PACKAGE_NAME = "@algosuite/vo-mcp";
|
|
791
|
+
var PACKAGE_SPEC_RE2 = /^@algosuite\/vo-mcp@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
792
|
+
var INTEGRITY_RE2 = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
793
|
+
var MAX_TARBALL_BYTES = 100 * 1024 * 1024;
|
|
794
|
+
var PUBLIC_REGISTRY = "https://registry.npmjs.org/";
|
|
795
|
+
function buildMinimalMaintenanceEnv(env = process.env, runtimeRoot = "") {
|
|
796
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
797
|
+
"PATH",
|
|
798
|
+
"Path",
|
|
799
|
+
"path",
|
|
800
|
+
"PATHEXT",
|
|
801
|
+
"SystemRoot",
|
|
802
|
+
"SYSTEMROOT",
|
|
803
|
+
"WINDIR",
|
|
804
|
+
"COMSPEC",
|
|
805
|
+
"TEMP",
|
|
806
|
+
"TMP",
|
|
807
|
+
"TMPDIR",
|
|
808
|
+
"HOME",
|
|
809
|
+
"USERPROFILE",
|
|
810
|
+
"APPDATA",
|
|
811
|
+
"LOCALAPPDATA",
|
|
812
|
+
"ProgramFiles",
|
|
813
|
+
"ProgramFiles(x86)",
|
|
814
|
+
"ProgramW6432",
|
|
815
|
+
"LANG",
|
|
816
|
+
"LC_ALL"
|
|
817
|
+
]);
|
|
818
|
+
const clean = {};
|
|
819
|
+
for (const [key, value] of Object.entries(env)) {
|
|
820
|
+
if (allowed.has(key) && typeof value === "string") clean[key] = value;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
...clean,
|
|
824
|
+
npm_config_ignore_scripts: "true",
|
|
825
|
+
npm_config_bin_links: "false",
|
|
826
|
+
npm_config_audit: "false",
|
|
827
|
+
npm_config_fund: "false",
|
|
828
|
+
npm_config_update_notifier: "false",
|
|
829
|
+
npm_config_registry: PUBLIC_REGISTRY,
|
|
830
|
+
...runtimeRoot ? {
|
|
831
|
+
npm_config_userconfig: join2(runtimeRoot, "maintenance", "user.npmrc"),
|
|
832
|
+
npm_config_globalconfig: join2(runtimeRoot, "maintenance", "global.npmrc"),
|
|
833
|
+
npm_config_cache: join2(runtimeRoot, "maintenance", "npm-cache")
|
|
834
|
+
} : {}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function defaultRun(command, args, options) {
|
|
838
|
+
return spawnSync2(command, args, {
|
|
839
|
+
cwd: options.cwd,
|
|
840
|
+
encoding: "utf8",
|
|
841
|
+
env: options.env,
|
|
842
|
+
shell: false,
|
|
843
|
+
windowsHide: true,
|
|
844
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
845
|
+
timeout: options.timeout ?? 12e4
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
function commandRunner({ platform, execPath, env, fileExists, run }) {
|
|
849
|
+
const npmCli = platform === "win32" ? resolveNpmCli({ env, execPath, fileExists }) : null;
|
|
850
|
+
if (platform === "win32" && !npmCli) throw new Error("trusted npm CLI not found");
|
|
851
|
+
const npmCommand = platform === "win32" ? execPath : "npm";
|
|
852
|
+
const prefix = npmCli ? [npmCli] : [];
|
|
853
|
+
return {
|
|
854
|
+
npm(args, options) {
|
|
855
|
+
return run(npmCommand, [...prefix, ...args], options);
|
|
856
|
+
},
|
|
857
|
+
node(args, options) {
|
|
858
|
+
return run(execPath, args, options);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function parseJsonOutput(result, operation) {
|
|
863
|
+
if (result?.status !== 0) {
|
|
864
|
+
throw new Error(`${operation} failed: ${String(result?.stderr || result?.error?.message || `exit ${result?.status ?? 1}`)}`);
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
return JSON.parse(String(result.stdout || ""));
|
|
868
|
+
} catch {
|
|
869
|
+
throw new Error(`${operation} returned invalid JSON`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function tarballIntegrity(file) {
|
|
873
|
+
return `sha512-${createHash2("sha512").update(readFileSync2(file)).digest("base64")}`;
|
|
874
|
+
}
|
|
875
|
+
function assertNoLinks(root) {
|
|
876
|
+
const pending = [root];
|
|
877
|
+
while (pending.length) {
|
|
878
|
+
const current = pending.pop();
|
|
879
|
+
const stat = lstatSync2(current);
|
|
880
|
+
if (stat.isSymbolicLink()) throw new Error("installed runtime contains a link/reparse point");
|
|
881
|
+
if (!stat.isDirectory()) continue;
|
|
882
|
+
for (const entry of readdirSync2(current)) pending.push(join2(current, entry));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function validateDependencyLock(payloadRoot, expected) {
|
|
886
|
+
const lock = JSON.parse(readFileSync2(join2(payloadRoot, "package-lock.json"), "utf8"));
|
|
887
|
+
if (Number(lock.lockfileVersion) < 3 || !lock.packages || typeof lock.packages !== "object") {
|
|
888
|
+
throw new Error("runtime dependency lock is missing or unsupported");
|
|
889
|
+
}
|
|
890
|
+
let foundPackage = false;
|
|
891
|
+
for (const [key, item] of Object.entries(lock.packages)) {
|
|
892
|
+
if (!key) continue;
|
|
893
|
+
if (item?.link === true) throw new Error(`runtime dependency lock contains link: ${key}`);
|
|
894
|
+
const isRunner = key.replaceAll("\\", "/").endsWith("node_modules/@algosuite/vo-mcp");
|
|
895
|
+
if (isRunner) {
|
|
896
|
+
foundPackage = item.version === expected.version && item.integrity === expected.integrity;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (!INTEGRITY_RE2.test(String(item?.integrity || ""))) throw new Error(`dependency lacks sha512 integrity: ${key}`);
|
|
900
|
+
if (!String(item?.resolved || "").startsWith("https://registry.npmjs.org/")) {
|
|
901
|
+
throw new Error(`dependency is not registry-pinned: ${key}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (!foundPackage) throw new Error("installed runner package does not match registry integrity");
|
|
905
|
+
}
|
|
906
|
+
function buildActive(slotId, metadata, paths) {
|
|
907
|
+
return {
|
|
908
|
+
slot_id: slotId,
|
|
909
|
+
version: metadata.version,
|
|
910
|
+
integrity: metadata.integrity,
|
|
911
|
+
entry_sha512: hashFileSha512(paths.entry),
|
|
912
|
+
supervisor_sha512: hashFileSha512(paths.supervisor),
|
|
913
|
+
tree_sha512: hashRuntimeTree(paths.slotRoot ?? paths.payloadRoot)
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function installSlot({ runtimeRoot, metadata, tarball, runner, npmEnv, runOptions, force }) {
|
|
917
|
+
const digest = createHash2("sha256").update(metadata.integrity).digest("hex").slice(0, 16);
|
|
918
|
+
const suffix = force ? `${digest}-${randomUUID2().slice(0, 8)}` : digest;
|
|
919
|
+
const slotId = `vo-mcp-${metadata.version}-${suffix}`;
|
|
920
|
+
const finalPaths = slotPaths(runtimeRoot, slotId);
|
|
921
|
+
if (!force && existsSync3(finalPaths.slotRoot)) {
|
|
922
|
+
const manifest = JSON.parse(readFileSync2(finalPaths.manifest, "utf8"));
|
|
923
|
+
const active = buildActive(slotId, metadata, finalPaths);
|
|
924
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
925
|
+
if (validated.ok && manifest.integrity === metadata.integrity) return { active, created: false };
|
|
926
|
+
}
|
|
927
|
+
const staging = join2(runtimeRoot, "staging", randomUUID2());
|
|
928
|
+
const payload = join2(staging, "payload");
|
|
929
|
+
let installedSlot = false;
|
|
930
|
+
try {
|
|
931
|
+
mkdirSync2(payload, { recursive: true });
|
|
932
|
+
writeFileSync2(join2(payload, "package.json"), `${JSON.stringify({ name: "algohq-runner-runtime", version: "0.0.0", private: true })}
|
|
933
|
+
`);
|
|
934
|
+
const install = runner.npm([
|
|
935
|
+
"install",
|
|
936
|
+
"--ignore-scripts",
|
|
937
|
+
"--no-bin-links",
|
|
938
|
+
"--no-audit",
|
|
939
|
+
"--no-fund",
|
|
940
|
+
"--package-lock=true",
|
|
941
|
+
"--save-exact",
|
|
942
|
+
`--registry=${PUBLIC_REGISTRY}`,
|
|
943
|
+
tarball
|
|
944
|
+
], { ...runOptions, cwd: payload, env: npmEnv, timeout: 18e4 });
|
|
945
|
+
if (install.status !== 0) throw new Error(`npm install failed: ${install.stderr || install.error?.message || install.status}`);
|
|
946
|
+
assertNoLinks(payload);
|
|
947
|
+
validateDependencyLock(payload, metadata);
|
|
948
|
+
const stagedPaths = {
|
|
949
|
+
entry: join2(payload, "node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp"),
|
|
950
|
+
supervisor: join2(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js"),
|
|
951
|
+
packageJson: join2(payload, "node_modules", "@algosuite", "vo-mcp", "package.json"),
|
|
952
|
+
credentialHelper: join2(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js"),
|
|
953
|
+
slotRoot: payload
|
|
954
|
+
};
|
|
955
|
+
const pkg = JSON.parse(readFileSync2(stagedPaths.packageJson, "utf8"));
|
|
956
|
+
if (pkg.name !== PACKAGE_NAME || pkg.version !== metadata.version) throw new Error("installed package identity mismatch");
|
|
957
|
+
if (!lstatSync2(stagedPaths.credentialHelper).isFile()) throw new Error("installed credential helper is missing");
|
|
958
|
+
const smoke = runner.node([stagedPaths.entry, "runner", "--version"], { ...runOptions, cwd: payload, env: npmEnv, timeout: 3e4 });
|
|
959
|
+
if (smoke.status !== 0 || String(smoke.stdout || "").trim() !== `vo-mcp runner ${metadata.version}`) {
|
|
960
|
+
throw new Error("bundled runtime smoke check failed");
|
|
961
|
+
}
|
|
962
|
+
const active = buildActive(slotId, metadata, stagedPaths);
|
|
963
|
+
atomicWriteJson(join2(payload, "runtime-manifest.json"), { schema_version: 1, ...active });
|
|
964
|
+
mkdirSync2(join2(runtimeRoot, "slots"), { recursive: true });
|
|
965
|
+
if (existsSync3(finalPaths.slotRoot)) throw new Error("immutable runtime slot already exists");
|
|
966
|
+
renameSync2(payload, finalPaths.slotRoot);
|
|
967
|
+
installedSlot = true;
|
|
968
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
969
|
+
if (!validated.ok) throw new Error(`staged runtime validation failed: ${validated.detail}`);
|
|
970
|
+
return { active, created: true };
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (installedSlot) rmSync2(finalPaths.slotRoot, { recursive: true, force: true });
|
|
973
|
+
throw error;
|
|
974
|
+
} finally {
|
|
975
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function stageBundledRuntimeSlot(options) {
|
|
979
|
+
const {
|
|
980
|
+
runtimeRoot,
|
|
981
|
+
packageSpec,
|
|
982
|
+
expectedVersion,
|
|
983
|
+
expectedIntegrity,
|
|
984
|
+
platform = process.platform,
|
|
985
|
+
execPath = process.execPath,
|
|
986
|
+
env = process.env,
|
|
987
|
+
fileExists = existsSync3,
|
|
988
|
+
run = defaultRun,
|
|
989
|
+
force = false
|
|
990
|
+
} = options;
|
|
991
|
+
if (!runtimeRoot || !isAbsolute2(runtimeRoot)) return { ok: false, status: 2, detail: "bundled runtime root unavailable" };
|
|
992
|
+
if (!expectedVersion || !PACKAGE_SPEC_RE2.test(`${PACKAGE_NAME}@${expectedVersion}`)) {
|
|
993
|
+
return { ok: false, status: 2, detail: "invalid expected runner version" };
|
|
994
|
+
}
|
|
995
|
+
if (!expectedIntegrity || !INTEGRITY_RE2.test(expectedIntegrity)) {
|
|
996
|
+
return { ok: false, status: 2, detail: "invalid expected runner integrity" };
|
|
997
|
+
}
|
|
998
|
+
const exactSpec = `${PACKAGE_NAME}@${expectedVersion}`;
|
|
999
|
+
if (packageSpec !== exactSpec) return { ok: false, status: 2, detail: "runner package spec does not match authorized version" };
|
|
1000
|
+
const resolvedRoot = resolve2(runtimeRoot);
|
|
1001
|
+
const npmEnv = buildMinimalMaintenanceEnv(env, resolvedRoot);
|
|
1002
|
+
const runOptions = { env: npmEnv, cwd: resolvedRoot };
|
|
1003
|
+
let tarDir = null;
|
|
1004
|
+
try {
|
|
1005
|
+
mkdirSync2(resolvedRoot, { recursive: true });
|
|
1006
|
+
mkdirSync2(join2(resolvedRoot, "maintenance"), { recursive: true });
|
|
1007
|
+
writeFileSync2(npmEnv.npm_config_userconfig, "", { mode: 384 });
|
|
1008
|
+
writeFileSync2(npmEnv.npm_config_globalconfig, "", { mode: 384 });
|
|
1009
|
+
const runner = commandRunner({ platform, execPath, env: npmEnv, fileExists, run });
|
|
1010
|
+
const metadata = { version: expectedVersion, integrity: expectedIntegrity };
|
|
1011
|
+
tarDir = join2(resolvedRoot, "staging", randomUUID2());
|
|
1012
|
+
mkdirSync2(tarDir, { recursive: true });
|
|
1013
|
+
const packed = parseJsonOutput(runner.npm([
|
|
1014
|
+
"pack",
|
|
1015
|
+
exactSpec,
|
|
1016
|
+
"--ignore-scripts",
|
|
1017
|
+
"--json",
|
|
1018
|
+
"--pack-destination",
|
|
1019
|
+
tarDir,
|
|
1020
|
+
`--registry=${PUBLIC_REGISTRY}`
|
|
1021
|
+
], runOptions), "npm pack");
|
|
1022
|
+
const record = Array.isArray(packed) ? packed[0] : packed;
|
|
1023
|
+
const tarball = join2(tarDir, basename(String(record?.filename || "")));
|
|
1024
|
+
if (!existsSync3(tarball) || !basename(tarball).endsWith(".tgz")) throw new Error("npm pack returned no tarball");
|
|
1025
|
+
if (lstatSync2(tarball).size > MAX_TARBALL_BYTES) throw new Error("runner package tarball exceeds size limit");
|
|
1026
|
+
if (record.integrity !== metadata.integrity || tarballIntegrity(tarball) !== metadata.integrity) {
|
|
1027
|
+
throw new Error("runner package sha512 integrity mismatch");
|
|
1028
|
+
}
|
|
1029
|
+
const installed = installSlot({ runtimeRoot: resolvedRoot, metadata, tarball, runner, npmEnv, runOptions, force });
|
|
1030
|
+
return { ok: true, status: 0, ...installed };
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
return { ok: false, status: 1, detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env) };
|
|
1033
|
+
} finally {
|
|
1034
|
+
if (tarDir) rmSync2(tarDir, { recursive: true, force: true });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function stageAndActivateBundledUpdate(options) {
|
|
1038
|
+
const staged = stageBundledRuntimeSlot(options);
|
|
1039
|
+
if (!staged.ok) return staged;
|
|
1040
|
+
try {
|
|
1041
|
+
activateSlot(resolve2(options.runtimeRoot), staged.active, options.action);
|
|
1042
|
+
return { ...staged, handoff: true };
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
return {
|
|
1045
|
+
ok: false,
|
|
1046
|
+
status: 1,
|
|
1047
|
+
detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), options.env ?? process.env)
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/runner/supervisor-activation.mjs
|
|
1053
|
+
var MAX_ACK_ATTEMPTS = 3;
|
|
1054
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1055
|
+
async function waitForAuthoritativeRunnerHeartbeat({
|
|
1056
|
+
client,
|
|
1057
|
+
runnerId: runnerId2,
|
|
1058
|
+
operatorId,
|
|
1059
|
+
runnerInstanceId,
|
|
1060
|
+
excludeRunnerInstanceId,
|
|
1061
|
+
daemonVersion,
|
|
1062
|
+
supervisorIdentity,
|
|
1063
|
+
timeoutMs = 6e4,
|
|
1064
|
+
pollMs = 1e3
|
|
1065
|
+
}) {
|
|
1066
|
+
if (!runnerInstanceId && !excludeRunnerInstanceId) throw new Error("runner instance identity proof unavailable");
|
|
1067
|
+
const deadline = Date.now() + timeoutMs;
|
|
1068
|
+
while (Date.now() < deadline) {
|
|
1069
|
+
try {
|
|
1070
|
+
const runners = await client.getRunnerStatus({ ...operatorId ? { operatorId } : {} });
|
|
1071
|
+
const runner = runners.find((item) => item?.runner_id === runnerId2 && item.status === "online" && (runnerInstanceId ? item.runner_meta?.runner_instance_id === runnerInstanceId : Boolean(item.runner_meta?.runner_instance_id) && item.runner_meta.runner_instance_id !== excludeRunnerInstanceId) && item.runner_meta?.daemon_version === daemonVersion && item.runner_meta?.supervisor_instance_id === supervisorIdentity.supervisorInstanceId && item.runner_meta?.supervisor_version === supervisorIdentity.supervisorVersion && supervisorIdentity.capabilities.every((value) => item.runner_meta?.supervisor_capabilities?.includes(value)) && Array.isArray(item.runner_meta?.available_agents) && item.runner_meta.available_agents.some((agent) => agent.agent === item.runner_meta.default_agent && agent.installed === true && agent.authenticated === true));
|
|
1072
|
+
if (runner) return runner;
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
await delay(pollMs);
|
|
1076
|
+
}
|
|
1077
|
+
throw new Error("authoritative child heartbeat attestation timed out");
|
|
1078
|
+
}
|
|
1079
|
+
async function finishPendingActivation({
|
|
1080
|
+
client,
|
|
1081
|
+
child,
|
|
1082
|
+
runtimeRoot,
|
|
1083
|
+
operatorId,
|
|
1084
|
+
runnerId: runnerId2,
|
|
1085
|
+
selfPath: selfPath2,
|
|
1086
|
+
packageVersion: packageVersion2,
|
|
1087
|
+
supervisorIdentity,
|
|
1088
|
+
waitForLocalRunner: waitForLocalRunner2,
|
|
1089
|
+
localStatus: localStatus2,
|
|
1090
|
+
waitForCloudRunner = waitForAuthoritativeRunnerHeartbeat,
|
|
1091
|
+
cloudTimeoutMs,
|
|
1092
|
+
cloudPollMs,
|
|
1093
|
+
stopChild: stopChild2,
|
|
1094
|
+
launchPreviousChild,
|
|
1095
|
+
log = () => {
|
|
1096
|
+
}
|
|
1097
|
+
}) {
|
|
1098
|
+
const pointer = runtimeRoot ? readActivation(runtimeRoot) : null;
|
|
1099
|
+
if (!pointer?.pending) return true;
|
|
1100
|
+
const runnerIdForAction = pointer.pending.runner_id || runnerId2;
|
|
1101
|
+
const operatorIdForAction = pointer.pending.operator_id || operatorId;
|
|
1102
|
+
if (pointer.pending.terminal_status === "failed") {
|
|
1103
|
+
try {
|
|
1104
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
1105
|
+
runnerId: runnerIdForAction,
|
|
1106
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
1107
|
+
...supervisorIdentity,
|
|
1108
|
+
status: "failed",
|
|
1109
|
+
detail: pointer.pending.terminal_detail
|
|
1110
|
+
});
|
|
1111
|
+
acknowledgeActivationFailure(runtimeRoot, pointer);
|
|
1112
|
+
return true;
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
log(`activation failure acknowledgement remains pending: ${error instanceof Error ? error.message : String(error)}`);
|
|
1115
|
+
await stopChild2(child);
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
let attestation;
|
|
1120
|
+
try {
|
|
1121
|
+
attestation = attestCurrentSupervisor({ runtimeRoot, selfPath: selfPath2, version: packageVersion2 });
|
|
1122
|
+
if (!attestation.ok) throw new Error(attestation.detail);
|
|
1123
|
+
if (!await waitForLocalRunner2(child)) throw new Error("activated runner did not become locally ready");
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
let detail = `activation attestation failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 400);
|
|
1126
|
+
let rolledBack = null;
|
|
1127
|
+
try {
|
|
1128
|
+
rolledBack = rollbackActivation(runtimeRoot, pointer, detail);
|
|
1129
|
+
} catch (rollbackError) {
|
|
1130
|
+
detail = `${detail}; rollback pending: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`.slice(0, 400);
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
1134
|
+
runnerId: runnerIdForAction,
|
|
1135
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
1136
|
+
...supervisorIdentity,
|
|
1137
|
+
status: "failed",
|
|
1138
|
+
detail
|
|
1139
|
+
});
|
|
1140
|
+
if (rolledBack) acknowledgeActivationFailure(runtimeRoot, rolledBack);
|
|
1141
|
+
} catch {
|
|
1142
|
+
} finally {
|
|
1143
|
+
await stopChild2(child);
|
|
1144
|
+
}
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
let activatedRunnerInstanceId = null;
|
|
1148
|
+
try {
|
|
1149
|
+
const status = await localStatus2();
|
|
1150
|
+
activatedRunnerInstanceId = status?.runnerInstanceId || null;
|
|
1151
|
+
await waitForCloudRunner({
|
|
1152
|
+
client,
|
|
1153
|
+
runnerId: runnerIdForAction,
|
|
1154
|
+
operatorId: operatorIdForAction,
|
|
1155
|
+
runnerInstanceId: status?.runnerInstanceId,
|
|
1156
|
+
daemonVersion: `vo-mcp/${attestation.active.version}`,
|
|
1157
|
+
supervisorIdentity,
|
|
1158
|
+
timeoutMs: cloudTimeoutMs,
|
|
1159
|
+
pollMs: cloudPollMs
|
|
1160
|
+
});
|
|
1161
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
1162
|
+
runnerId: runnerIdForAction,
|
|
1163
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
1164
|
+
...supervisorIdentity,
|
|
1165
|
+
status: "succeeded",
|
|
1166
|
+
detail: `attested new supervisor ${attestation.active.version} ${attestation.active.integrity}`
|
|
1167
|
+
});
|
|
1168
|
+
finalizeActivation(runtimeRoot, attestation.pointer);
|
|
1169
|
+
return true;
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
const failure = `activation acknowledgement failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 400);
|
|
1172
|
+
let retry;
|
|
1173
|
+
try {
|
|
1174
|
+
retry = recordActivationRetry(runtimeRoot, attestation.pointer, failure);
|
|
1175
|
+
} catch (retryError) {
|
|
1176
|
+
log(`activation retry could not be recorded: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
|
|
1177
|
+
await stopChild2(child);
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
if (retry.pending.ack_attempts < MAX_ACK_ATTEMPTS) {
|
|
1181
|
+
log(`activation acknowledgement pending (${retry.pending.ack_attempts}/${MAX_ACK_ATTEMPTS})`);
|
|
1182
|
+
await stopChild2(child);
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
const previous = validateSlot(runtimeRoot, retry.previous);
|
|
1186
|
+
let detail = `${failure}; retry limit reached; previous runtime restored`.slice(0, 400);
|
|
1187
|
+
let rollbackChild = null;
|
|
1188
|
+
let rolledBack = null;
|
|
1189
|
+
try {
|
|
1190
|
+
if (!previous.ok) throw new Error(`previous runtime invalid: ${previous.detail}`, { cause: error });
|
|
1191
|
+
rolledBack = rollbackActivation(runtimeRoot, retry, detail);
|
|
1192
|
+
await stopChild2(child);
|
|
1193
|
+
rollbackChild = launchPreviousChild(previous.paths.entry);
|
|
1194
|
+
if (!await waitForLocalRunner2(rollbackChild)) throw new Error("previous runner did not become locally ready", { cause: error });
|
|
1195
|
+
const status = await localStatus2();
|
|
1196
|
+
await waitForCloudRunner({
|
|
1197
|
+
client,
|
|
1198
|
+
runnerId: runnerIdForAction,
|
|
1199
|
+
operatorId: operatorIdForAction,
|
|
1200
|
+
runnerInstanceId: status?.runnerInstanceId,
|
|
1201
|
+
excludeRunnerInstanceId: status?.runnerInstanceId ? void 0 : activatedRunnerInstanceId,
|
|
1202
|
+
daemonVersion: `vo-mcp/${retry.previous.version}`,
|
|
1203
|
+
supervisorIdentity,
|
|
1204
|
+
timeoutMs: cloudTimeoutMs,
|
|
1205
|
+
pollMs: cloudPollMs
|
|
1206
|
+
});
|
|
1207
|
+
} catch (rollbackError) {
|
|
1208
|
+
detail = `${detail}; rollback proof failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`.slice(0, 400);
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
await client.completeRunnerControl(retry.pending.action_id, {
|
|
1212
|
+
runnerId: runnerIdForAction,
|
|
1213
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
1214
|
+
...supervisorIdentity,
|
|
1215
|
+
status: "failed",
|
|
1216
|
+
detail
|
|
1217
|
+
});
|
|
1218
|
+
if (rolledBack) acknowledgeActivationFailure(runtimeRoot, rolledBack);
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
if (rollbackChild) await stopChild2(rollbackChild);
|
|
1222
|
+
else await stopChild2(child);
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
390
1227
|
// src/runner/supervisor-child-env.mjs
|
|
391
1228
|
import { hostname as systemHostname } from "node:os";
|
|
392
1229
|
|
|
@@ -567,23 +1404,24 @@ async function prepareSupervisorAuth({
|
|
|
567
1404
|
}
|
|
568
1405
|
|
|
569
1406
|
// src/runner/supervisor-credential-reader.mjs
|
|
570
|
-
import { spawnSync as
|
|
571
|
-
import { existsSync } from "node:fs";
|
|
572
|
-
import { dirname, join } from "node:path";
|
|
1407
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1408
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1409
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
573
1410
|
import { fileURLToPath } from "node:url";
|
|
574
1411
|
function defaultCredentialHelperPath(metaUrl = import.meta.url) {
|
|
575
|
-
const moduleDir =
|
|
576
|
-
const bundled =
|
|
577
|
-
const source =
|
|
578
|
-
return
|
|
1412
|
+
const moduleDir = dirname2(fileURLToPath(metaUrl));
|
|
1413
|
+
const bundled = join3(moduleDir, "supervisor-credential-helper.js");
|
|
1414
|
+
const source = join3(moduleDir, "..", "supervisor-credential-helper.mjs");
|
|
1415
|
+
return existsSync4(source) ? source : bundled;
|
|
579
1416
|
}
|
|
580
1417
|
function readStoredCredentialIsolated({
|
|
581
|
-
spawn: spawn2 =
|
|
1418
|
+
spawn: spawn2 = spawnSync3,
|
|
582
1419
|
execPath = process.execPath,
|
|
583
1420
|
helperPath = defaultCredentialHelperPath(),
|
|
1421
|
+
helperArgs = [],
|
|
584
1422
|
env = process.env
|
|
585
1423
|
} = {}) {
|
|
586
|
-
const result = spawn2(execPath, [helperPath], {
|
|
1424
|
+
const result = spawn2(execPath, [helperPath, ...helperArgs], {
|
|
587
1425
|
env,
|
|
588
1426
|
encoding: "utf8",
|
|
589
1427
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -606,8 +1444,11 @@ function readStoredCredentialIsolated({
|
|
|
606
1444
|
var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
607
1445
|
var POLL_MS = 5e3;
|
|
608
1446
|
var CHILD_START_MS = 1500;
|
|
609
|
-
var
|
|
610
|
-
var
|
|
1447
|
+
var SUPERVISOR_CAPABILITIES = ["bundled-runtime-slots-v1"];
|
|
1448
|
+
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 = fileURLToPath2(import.meta.url);
|
|
1450
|
+
var childEntry = join4(dirname3(selfPath), "runner-cli.js");
|
|
1451
|
+
var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
611
1452
|
var runnerId = resolveSupervisorRunnerId(process.env);
|
|
612
1453
|
function packageVersion() {
|
|
613
1454
|
try {
|
|
@@ -616,6 +1457,17 @@ function packageVersion() {
|
|
|
616
1457
|
return "unknown";
|
|
617
1458
|
}
|
|
618
1459
|
}
|
|
1460
|
+
var requestedSupervisorInstanceId = UUID_RE2.test(String(process.env.VO_RUNNER_SUPERVISOR_INSTANCE_ID || "")) ? process.env.VO_RUNNER_SUPERVISOR_INSTANCE_ID : randomUUID3();
|
|
1461
|
+
var supervisorInstanceId = activationSupervisorInstanceId(
|
|
1462
|
+
runtimeRootFromEnv(process.env),
|
|
1463
|
+
requestedSupervisorInstanceId
|
|
1464
|
+
);
|
|
1465
|
+
var supervisorVersion = packageVersion();
|
|
1466
|
+
var supervisorControlIdentity = {
|
|
1467
|
+
supervisorInstanceId,
|
|
1468
|
+
supervisorVersion,
|
|
1469
|
+
capabilities: SUPERVISOR_CAPABILITIES
|
|
1470
|
+
};
|
|
619
1471
|
function spawnChild(childEnv) {
|
|
620
1472
|
return spawn(process.execPath, [childEntry], {
|
|
621
1473
|
env: childEnv,
|
|
@@ -623,6 +1475,13 @@ function spawnChild(childEnv) {
|
|
|
623
1475
|
windowsHide: true
|
|
624
1476
|
});
|
|
625
1477
|
}
|
|
1478
|
+
function spawnPreviousChild(entry, childEnv) {
|
|
1479
|
+
return spawn(process.execPath, [entry, "runner"], {
|
|
1480
|
+
env: childEnv,
|
|
1481
|
+
stdio: "inherit",
|
|
1482
|
+
windowsHide: true
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
626
1485
|
async function localStatus() {
|
|
627
1486
|
try {
|
|
628
1487
|
const port = Number(process.env.VO_CODE_RUNNER_CONTROL_PORT || 7787);
|
|
@@ -638,7 +1497,8 @@ async function waitForLocalRunner(child) {
|
|
|
638
1497
|
const deadline = Date.now() + 15e3;
|
|
639
1498
|
while (Date.now() < deadline) {
|
|
640
1499
|
if (child.exitCode !== null) return false;
|
|
641
|
-
|
|
1500
|
+
const status = await localStatus();
|
|
1501
|
+
if (status?.running === true && Number(status.pid) === Number(child.pid)) return true;
|
|
642
1502
|
await sleep(500);
|
|
643
1503
|
}
|
|
644
1504
|
return false;
|
|
@@ -647,7 +1507,7 @@ async function stopChild(child) {
|
|
|
647
1507
|
if (!child || child.exitCode !== null) return;
|
|
648
1508
|
child.kill(process.platform === "win32" ? void 0 : "SIGTERM");
|
|
649
1509
|
await Promise.race([
|
|
650
|
-
new Promise((
|
|
1510
|
+
new Promise((resolve3) => child.once("exit", resolve3)),
|
|
651
1511
|
sleep(15e3)
|
|
652
1512
|
]);
|
|
653
1513
|
if (child.exitCode === null) child.kill("SIGKILL");
|
|
@@ -660,14 +1520,43 @@ async function main() {
|
|
|
660
1520
|
storedCredential: stored,
|
|
661
1521
|
controlPlaneUrl
|
|
662
1522
|
});
|
|
1523
|
+
Object.assign(childEnv, {
|
|
1524
|
+
VO_RUNNER_SUPERVISOR_INSTANCE_ID: supervisorInstanceId,
|
|
1525
|
+
VO_RUNNER_SUPERVISOR_VERSION: supervisorVersion,
|
|
1526
|
+
VO_RUNNER_SUPERVISOR_CAPABILITIES: SUPERVISOR_CAPABILITIES.join(",")
|
|
1527
|
+
});
|
|
663
1528
|
const client = createControlPlaneClient({ baseUrl: controlPlaneUrl, env: clientEnv });
|
|
664
|
-
|
|
1529
|
+
const runtimeRoot = runtimeRootFromEnv(process.env);
|
|
1530
|
+
let child = null;
|
|
665
1531
|
let stopping = false;
|
|
666
1532
|
let handling = false;
|
|
667
1533
|
const respawn = () => {
|
|
668
|
-
if (!stopping && !handling && (!child || child.exitCode !== null)) child =
|
|
1534
|
+
if (!stopping && !handling && (!child || child.exitCode !== null)) child = launchChild();
|
|
669
1535
|
};
|
|
670
|
-
|
|
1536
|
+
const launchChild = () => {
|
|
1537
|
+
const next = spawnChild(childEnv);
|
|
1538
|
+
next.on("exit", () => setTimeout(respawn, 2e3));
|
|
1539
|
+
return next;
|
|
1540
|
+
};
|
|
1541
|
+
child = launchChild();
|
|
1542
|
+
if (!await finishPendingActivation({
|
|
1543
|
+
client,
|
|
1544
|
+
child,
|
|
1545
|
+
runtimeRoot,
|
|
1546
|
+
operatorId,
|
|
1547
|
+
runnerId,
|
|
1548
|
+
selfPath,
|
|
1549
|
+
packageVersion: supervisorVersion,
|
|
1550
|
+
supervisorIdentity: supervisorControlIdentity,
|
|
1551
|
+
waitForLocalRunner,
|
|
1552
|
+
localStatus,
|
|
1553
|
+
stopChild,
|
|
1554
|
+
launchPreviousChild: (entry) => spawnPreviousChild(entry, childEnv),
|
|
1555
|
+
log: (message) => console.error(`[vo-runner supervisor] ${message}`)
|
|
1556
|
+
})) {
|
|
1557
|
+
stopping = true;
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
671
1560
|
const shutdown = async () => {
|
|
672
1561
|
if (stopping) return;
|
|
673
1562
|
stopping = true;
|
|
@@ -681,7 +1570,11 @@ async function main() {
|
|
|
681
1570
|
});
|
|
682
1571
|
while (!stopping) {
|
|
683
1572
|
try {
|
|
684
|
-
const action = await client.pollRunnerControl({
|
|
1573
|
+
const action = await client.pollRunnerControl({
|
|
1574
|
+
runnerId,
|
|
1575
|
+
...operatorId ? { operatorId } : {},
|
|
1576
|
+
...supervisorControlIdentity
|
|
1577
|
+
});
|
|
685
1578
|
if (!action) {
|
|
686
1579
|
await sleep(POLL_MS);
|
|
687
1580
|
continue;
|
|
@@ -692,6 +1585,7 @@ async function main() {
|
|
|
692
1585
|
await client.completeRunnerControl(action.actionId, {
|
|
693
1586
|
runnerId,
|
|
694
1587
|
...operatorId ? { operatorId } : {},
|
|
1588
|
+
...supervisorControlIdentity,
|
|
695
1589
|
status: "failed",
|
|
696
1590
|
detail: `deferred safely: ${beforeStop.activeTasks} active task(s); retry when the runner is idle`
|
|
697
1591
|
});
|
|
@@ -699,16 +1593,35 @@ async function main() {
|
|
|
699
1593
|
continue;
|
|
700
1594
|
}
|
|
701
1595
|
await stopChild(child);
|
|
702
|
-
const
|
|
1596
|
+
const bundledAction = action.kind === "update" || action.kind === "reinstall";
|
|
1597
|
+
const result = bundledAction ? stageAndActivateBundledUpdate({
|
|
1598
|
+
runtimeRoot,
|
|
1599
|
+
packageSpec: `@algosuite/vo-mcp@${action.desired_package_version}`,
|
|
1600
|
+
expectedVersion: action.desired_package_version,
|
|
1601
|
+
expectedIntegrity: action.desired_package_integrity,
|
|
1602
|
+
action: {
|
|
1603
|
+
actionId: action.actionId,
|
|
1604
|
+
runnerId,
|
|
1605
|
+
operatorId: operatorId || "",
|
|
1606
|
+
supervisorInstanceId
|
|
1607
|
+
},
|
|
1608
|
+
env: process.env,
|
|
1609
|
+
force: action.kind === "reinstall"
|
|
1610
|
+
}) : runHostMaintenance(action.kind, {
|
|
703
1611
|
env: clientEnv,
|
|
704
1612
|
log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
|
|
705
1613
|
});
|
|
706
|
-
|
|
1614
|
+
if (result.ok && result.handoff) {
|
|
1615
|
+
console.warn(`[vo-runner supervisor] activated ${result.active.version}; exiting for new-process attestation`);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
child = launchChild();
|
|
707
1619
|
await sleep(CHILD_START_MS);
|
|
708
1620
|
if (child.exitCode !== null || !await waitForLocalRunner(child)) result.ok = false;
|
|
709
1621
|
await client.completeRunnerControl(action.actionId, {
|
|
710
1622
|
runnerId,
|
|
711
1623
|
...operatorId ? { operatorId } : {},
|
|
1624
|
+
...supervisorControlIdentity,
|
|
712
1625
|
status: result.ok ? "succeeded" : "failed",
|
|
713
1626
|
detail: result.ok ? `runner ${packageVersion()} reconnected` : `maintenance exited ${result.status}${result.detail ? `: ${result.detail}` : ""}`
|
|
714
1627
|
});
|