@algosuite/vo-mcp 0.2.0-beta.4 → 0.2.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/bin/vo-mcp +9 -3
- package/dist/agent-auth-probe-cli.mjs +1707 -0
- package/dist/autostart-cli.js +115 -60
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +107511 -0
- package/dist/cli.js +2384 -339
- package/dist/cli.js.map +4 -4
- package/dist/index.js +2118 -199
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +361 -345
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +13987 -2596
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +2617 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +81 -5
- package/dist/set-key-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +125 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +5 -3
|
@@ -0,0 +1,2617 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/runner/control-plane-auth-stub.mjs
|
|
18
|
+
var control_plane_auth_stub_exports = {};
|
|
19
|
+
__export(control_plane_auth_stub_exports, {
|
|
20
|
+
getFirebaseAuth: () => getFirebaseAuth
|
|
21
|
+
});
|
|
22
|
+
async function getFirebaseAuth() {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"vo-mcp runner: no control-plane credential. Run `vo-mcp login` first \u2014 the runner authenticates with your stored vo_credential (or set VO_CONTROL_PLANE_ADMIN_TOKEN)."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
var init_control_plane_auth_stub = __esm({
|
|
28
|
+
"src/runner/control-plane-auth-stub.mjs"() {
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/runner-supervisor.mjs
|
|
33
|
+
import { spawn } from "node:child_process";
|
|
34
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
35
|
+
import { createRequire } from "node:module";
|
|
36
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
37
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
38
|
+
|
|
39
|
+
// ../../scripts/virtual-office/code-runner/installation-token.mjs
|
|
40
|
+
var READ_TOKEN_TIMEOUT_MS = 15e3;
|
|
41
|
+
async function fetchInstallationToken({ req, required = false, readOnly = false, repo = null }) {
|
|
42
|
+
const fail = (reason) => {
|
|
43
|
+
if (required) throw new Error(`installation-token required: ${reason}`);
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
const res = await req(
|
|
48
|
+
"POST",
|
|
49
|
+
"/api/v1/github/installation-token",
|
|
50
|
+
readOnly ? { scope: "read", ...repo ? { repo } : {} } : {},
|
|
51
|
+
readOnly ? { timeoutMs: READ_TOKEN_TIMEOUT_MS } : {}
|
|
52
|
+
);
|
|
53
|
+
if (!res.ok) return fail(`HTTP ${res.status}`);
|
|
54
|
+
const json = await res.json();
|
|
55
|
+
if (!json || !json.token) return fail("missing token");
|
|
56
|
+
if (readOnly && json.scope !== "read") return fail("control plane did not confirm a read-only grant");
|
|
57
|
+
return { token: json.token, expiresAt: json.expires_at || null };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (required) throw err;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ../../scripts/virtual-office/code-runner/control-plane-task-list.mjs
|
|
65
|
+
var PAGE_SIZE = 500;
|
|
66
|
+
async function listAllPrOpenedTasks(request) {
|
|
67
|
+
const tasks = [];
|
|
68
|
+
let beforeCreatedAt = "";
|
|
69
|
+
let beforeId = "";
|
|
70
|
+
for (; ; ) {
|
|
71
|
+
const params = new URLSearchParams({
|
|
72
|
+
status: "pr_opened",
|
|
73
|
+
limit: String(PAGE_SIZE),
|
|
74
|
+
runner_adoption: "1"
|
|
75
|
+
});
|
|
76
|
+
if (beforeCreatedAt) {
|
|
77
|
+
params.set("before_created_at", beforeCreatedAt);
|
|
78
|
+
params.set("before_id", beforeId);
|
|
79
|
+
}
|
|
80
|
+
const res = await request("GET", `/api/v1/code-task?${params}`);
|
|
81
|
+
if (!res.ok) throw new Error(`listPrOpenedTasks failed: HTTP ${res.status}`);
|
|
82
|
+
const json = await res.json();
|
|
83
|
+
const page = Array.isArray(json?.tasks) ? json.tasks : [];
|
|
84
|
+
tasks.push(...page);
|
|
85
|
+
if (page.length < PAGE_SIZE) return tasks;
|
|
86
|
+
const last = page.at(-1);
|
|
87
|
+
if (!last?.created_at || !last?.code_task_id) {
|
|
88
|
+
throw new Error("listPrOpenedTasks pagination cursor missing");
|
|
89
|
+
}
|
|
90
|
+
beforeCreatedAt = last.created_at;
|
|
91
|
+
beforeId = last.code_task_id;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ../../scripts/virtual-office/code-runner/control-plane-resume.mjs
|
|
96
|
+
async function resumeCodeTaskRequest(req, taskId, { automaticRateLimit = false, automaticContinuation = false } = {}, onUnauthorized = () => {
|
|
97
|
+
}) {
|
|
98
|
+
const res = await req(
|
|
99
|
+
"POST",
|
|
100
|
+
`/api/v1/code-task/${encodeURIComponent(taskId)}/resume`,
|
|
101
|
+
automaticRateLimit ? { automatic_rate_limit: true } : automaticContinuation ? { automatic_continuation: true } : {}
|
|
102
|
+
);
|
|
103
|
+
if (res.status === 401) {
|
|
104
|
+
onUnauthorized();
|
|
105
|
+
throw new Error("resume unauthorized (401)");
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok) throw new Error(`resume failed: HTTP ${res.status}`);
|
|
108
|
+
const json = await res.json();
|
|
109
|
+
return json && json.task ? json.task : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ../../scripts/virtual-office/code-runner/control-plane-autonomous-admission.mjs
|
|
113
|
+
function makeAutonomousDispatchAdmissionClient(req, timeoutMs, onUnauthorized = () => {
|
|
114
|
+
}) {
|
|
115
|
+
return {
|
|
116
|
+
async reserveAutonomousDispatchBudget({ requestedBudgetUsd, reservationId, occurrenceKey }) {
|
|
117
|
+
const res = await req("POST", "/api/v1/autonomous-dispatch/admission", {
|
|
118
|
+
requested_budget_usd: requestedBudgetUsd,
|
|
119
|
+
reservation_id: reservationId,
|
|
120
|
+
dispatch_occurrence_key: occurrenceKey
|
|
121
|
+
}, { timeoutMs });
|
|
122
|
+
if (res.status === 401) {
|
|
123
|
+
onUnauthorized();
|
|
124
|
+
throw new Error("autonomous dispatch admission unauthorized (401)");
|
|
125
|
+
}
|
|
126
|
+
if (!res.ok) throw new Error(`autonomous dispatch admission failed: HTTP ${res.status}`);
|
|
127
|
+
const body = await res.json();
|
|
128
|
+
return {
|
|
129
|
+
allowed: body?.allowed === true,
|
|
130
|
+
reason: typeof body?.reason === "string" ? body.reason : ""
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
async releaseAutonomousDispatchBudget(reservationId) {
|
|
134
|
+
const res = await req("POST", "/api/v1/autonomous-dispatch/reservation/release", {
|
|
135
|
+
reservation_id: reservationId
|
|
136
|
+
}, { timeoutMs });
|
|
137
|
+
if (res.status === 401) {
|
|
138
|
+
onUnauthorized();
|
|
139
|
+
throw new Error("autonomous dispatch release unauthorized (401)");
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) throw new Error(`autonomous dispatch release failed: HTTP ${res.status}`);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ../../scripts/virtual-office/code-runner/control-plane-merge.mjs
|
|
148
|
+
async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauthorized) {
|
|
149
|
+
const res = await req(
|
|
150
|
+
"POST",
|
|
151
|
+
"/api/v1/admin/pr/merge",
|
|
152
|
+
{ prNumber, automationContext },
|
|
153
|
+
{ timeoutMs: 12e4 }
|
|
154
|
+
);
|
|
155
|
+
if (res.status === 401) {
|
|
156
|
+
onUnauthorized();
|
|
157
|
+
throw new Error("gated merge unauthorized (401)");
|
|
158
|
+
}
|
|
159
|
+
const json = await res.json().catch(() => ({}));
|
|
160
|
+
if (res.ok && json?.ok === true) {
|
|
161
|
+
const result = json?.result && typeof json.result === "object" ? json.result : {};
|
|
162
|
+
const status = result.merged === true || result.status === "merged" ? "merged" : result.status === "auto-merge-enabled" || String(result.action || "").includes("auto-merge") ? "queued" : "accepted";
|
|
163
|
+
return {
|
|
164
|
+
status,
|
|
165
|
+
detail: typeof result.detail === "string" ? result.detail : null,
|
|
166
|
+
actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (res.status === 503 && json?.error === "verify_unavailable") {
|
|
170
|
+
return { status: "retry", reason: json.reason || "verification unavailable" };
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
status: "blocked",
|
|
174
|
+
reason: json?.reason || json?.message || json?.error || `HTTP ${res.status}`,
|
|
175
|
+
actionReceiptId: typeof json?.action_receipt_id === "string" ? json.action_receipt_id : null
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ../../scripts/virtual-office/code-runner/claim-gate-notice.mjs
|
|
180
|
+
var REASON_HELP = {
|
|
181
|
+
daemon_version_below_floor: "this daemon is older than the approved release target \u2014 it idles until the governed updater brings it current (operator override: VO_RUNNER_CLAIM_MIN_DAEMON_VERSION / VO_RUNNER_CLAIM_VERSION_GATE=off on the control plane)",
|
|
182
|
+
daemon_version_unreported: "this daemon reports no parseable version in its heartbeat \u2014 too old for the updater to manage, so it may not claim work",
|
|
183
|
+
no_fresh_heartbeat: "the control plane has no fresh heartbeat from this runner \u2014 claims resume once heartbeats land",
|
|
184
|
+
runner_denylisted: "this runner id is on the operator quarantine list (VO_RUNNER_CLAIM_DENYLIST)"
|
|
185
|
+
};
|
|
186
|
+
function describeClaimGate(gate) {
|
|
187
|
+
if (!gate || gate.allowed !== false) return null;
|
|
188
|
+
const reason = String(gate.reason || "denied");
|
|
189
|
+
const floor = gate.floor_version ? ` (floor ${gate.floor_version})` : "";
|
|
190
|
+
return `claim gate: DENIED \u2014 ${reason}${floor}: ${(Object.hasOwn(REASON_HELP, reason) ? REASON_HELP[reason] : null) ?? "the control plane refused this runner's claims"}`;
|
|
191
|
+
}
|
|
192
|
+
function makeClaimGateNotice({ log = () => {
|
|
193
|
+
} } = {}) {
|
|
194
|
+
let last = null;
|
|
195
|
+
let current = null;
|
|
196
|
+
return {
|
|
197
|
+
current: () => current,
|
|
198
|
+
observe(json) {
|
|
199
|
+
const gate = json && typeof json === "object" ? json.claim_gate : null;
|
|
200
|
+
const denied = gate && gate.allowed === false ? gate : null;
|
|
201
|
+
current = denied ? { ...denied, observed_at: (/* @__PURE__ */ new Date()).toISOString() } : null;
|
|
202
|
+
const signature = denied ? `${denied.reason}|${denied.floor_version ?? ""}` : null;
|
|
203
|
+
if (signature === last) return;
|
|
204
|
+
if (denied) log(describeClaimGate(denied));
|
|
205
|
+
else if (last !== null) log("claim gate: allowed again \u2014 this runner may claim work");
|
|
206
|
+
last = signature;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ../../scripts/virtual-office/code-runner/control-plane-client.mjs
|
|
212
|
+
var cachedFirebaseToken = null;
|
|
213
|
+
var ClaimAuthorityChangedError = class extends Error {
|
|
214
|
+
constructor() {
|
|
215
|
+
super("code-task claim authority changed");
|
|
216
|
+
this.name = "ClaimAuthorityChangedError";
|
|
217
|
+
this.code = "code_task_claim_authority_changed";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
async function resolveBearer(env) {
|
|
221
|
+
const adminToken = env.VO_CONTROL_PLANE_ADMIN_TOKEN;
|
|
222
|
+
if (adminToken) return adminToken;
|
|
223
|
+
if (cachedFirebaseToken) return cachedFirebaseToken;
|
|
224
|
+
const { getFirebaseAuth: getFirebaseAuth2 } = await Promise.resolve().then(() => (init_control_plane_auth_stub(), control_plane_auth_stub_exports));
|
|
225
|
+
const auth = await getFirebaseAuth2({ env });
|
|
226
|
+
if (!auth || !auth.idToken) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"no control-plane credential: set VO_CONTROL_PLANE_ADMIN_TOKEN, or SMOKE_EMAIL/SMOKE_PASSWORD/SMOKE_API_KEY"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
cachedFirebaseToken = auth.idToken;
|
|
232
|
+
return cachedFirebaseToken;
|
|
233
|
+
}
|
|
234
|
+
function createControlPlaneClient({
|
|
235
|
+
baseUrl,
|
|
236
|
+
env = process.env,
|
|
237
|
+
fetchImpl = fetch,
|
|
238
|
+
heartbeatTimeoutMs = Math.min(
|
|
239
|
+
Math.max(Number(env.VO_CODE_RUNNER_HEARTBEAT_TIMEOUT_MS) || 15e3, 1e3),
|
|
240
|
+
6e4
|
|
241
|
+
),
|
|
242
|
+
taskRequestTimeoutMs = Math.min(
|
|
243
|
+
Math.max(Number(env.VO_CODE_RUNNER_TASK_REQUEST_TIMEOUT_MS) || 5e3, 100),
|
|
244
|
+
6e4
|
|
245
|
+
),
|
|
246
|
+
runnerId: runnerId2,
|
|
247
|
+
runnerInstanceId
|
|
248
|
+
} = {}) {
|
|
249
|
+
const resolvedBaseUrl = baseUrl ?? env.VO_CONTROL_PLANE_URL ?? "";
|
|
250
|
+
if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
|
|
251
|
+
const root = resolvedBaseUrl.replace(/\/+$/, "");
|
|
252
|
+
async function req(method, path2, body, { timeoutMs } = {}) {
|
|
253
|
+
const bearer = await resolveBearer(env);
|
|
254
|
+
const controller = timeoutMs ? new AbortController() : null;
|
|
255
|
+
let timeoutId;
|
|
256
|
+
const request = Promise.resolve(fetchImpl(`${root}${path2}`, {
|
|
257
|
+
method,
|
|
258
|
+
headers: {
|
|
259
|
+
"content-type": "application/json",
|
|
260
|
+
authorization: `Bearer ${bearer}`
|
|
261
|
+
},
|
|
262
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
263
|
+
...controller ? { signal: controller.signal } : {}
|
|
264
|
+
}));
|
|
265
|
+
if (!timeoutMs) return request;
|
|
266
|
+
const timeout = new Promise((_, reject) => {
|
|
267
|
+
timeoutId = setTimeout(() => {
|
|
268
|
+
controller.abort();
|
|
269
|
+
reject(new Error(`control-plane ${path2} timed out after ${timeoutMs}ms`));
|
|
270
|
+
}, timeoutMs);
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
return await Promise.race([request, timeout]);
|
|
274
|
+
} finally {
|
|
275
|
+
clearTimeout(timeoutId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const taskReq = (method, path2, body, options = {}) => req(method, path2, body, { timeoutMs: taskRequestTimeoutMs, ...options });
|
|
279
|
+
const claimGate = makeClaimGateNotice({ log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`) });
|
|
280
|
+
return {
|
|
281
|
+
getClaimGate: () => claimGate.current(),
|
|
282
|
+
// last DENIED claim-gate verdict (null when allowed) — for /status + tests
|
|
283
|
+
...makeAutonomousDispatchAdmissionClient(
|
|
284
|
+
req,
|
|
285
|
+
taskRequestTimeoutMs,
|
|
286
|
+
() => {
|
|
287
|
+
cachedFirebaseToken = null;
|
|
288
|
+
}
|
|
289
|
+
),
|
|
290
|
+
/**
|
|
291
|
+
* Claim the next pending task. Returns the task or null (empty queue).
|
|
292
|
+
* `repos` (optional `owner/name` list) and `operatorIds` (optional
|
|
293
|
+
* `operator_id` list) scope the claim so this daemon only picks up tasks it
|
|
294
|
+
* serves — the control-plane filters by both (logical AND), so another
|
|
295
|
+
* operator's task never lands on (or bills) this machine.
|
|
296
|
+
*/
|
|
297
|
+
async claim(runnerId3, repos, operatorIds, session = {}) {
|
|
298
|
+
const body = { runner_id: runnerId3 };
|
|
299
|
+
if (Array.isArray(repos) && repos.length > 0) body.repos = repos;
|
|
300
|
+
if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
|
|
301
|
+
if (session.runnerInstanceId) {
|
|
302
|
+
body.runner_instance_id = session.runnerInstanceId;
|
|
303
|
+
body.runner_progress_protocol_version = 2;
|
|
304
|
+
}
|
|
305
|
+
if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
|
|
306
|
+
if (session.defaultAgent) body.default_agent = session.defaultAgent;
|
|
307
|
+
if (Array.isArray(session.availableAgents)) {
|
|
308
|
+
body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
|
|
309
|
+
}
|
|
310
|
+
const res = await taskReq("POST", "/api/v1/code-task/claim", body);
|
|
311
|
+
if (res.status === 401) {
|
|
312
|
+
cachedFirebaseToken = null;
|
|
313
|
+
throw new Error("claim unauthorized (401)");
|
|
314
|
+
}
|
|
315
|
+
if (!res.ok) throw new Error(`claim failed: HTTP ${res.status}`);
|
|
316
|
+
const json = await res.json();
|
|
317
|
+
claimGate.observe(json);
|
|
318
|
+
return json && json.task ? json.task : null;
|
|
319
|
+
},
|
|
320
|
+
/**
|
|
321
|
+
* Enqueue a new code-task (used by the PR watcher to auto-dispatch a CI fix).
|
|
322
|
+
* Server derives operator/tenant from the daemon's authenticated principal.
|
|
323
|
+
* Returns the created task, or throws on a non-2xx response.
|
|
324
|
+
*/
|
|
325
|
+
async enqueueCodeTask({ repo, prompt, max_budget_usd, max_turns, dispatch_mode, tier, agent, model, dispatch_occurrence_key, autonomous_reservation_id, on_behalf_of_operator_id, repair_pr_number, repair_kind, repair_head_sha, repair_chain }) {
|
|
326
|
+
const body = { repo, prompt };
|
|
327
|
+
if (typeof max_budget_usd === "number") body.max_budget_usd = max_budget_usd;
|
|
328
|
+
if (typeof max_turns === "number") body.max_turns = max_turns;
|
|
329
|
+
for (const [key, value] of Object.entries({ dispatch_mode, tier, agent, model, repair_kind, repair_head_sha })) {
|
|
330
|
+
if (value) body[key] = value;
|
|
331
|
+
}
|
|
332
|
+
if (dispatch_occurrence_key) body.dispatch_occurrence_key = dispatch_occurrence_key;
|
|
333
|
+
if (autonomous_reservation_id) body.autonomous_reservation_id = autonomous_reservation_id;
|
|
334
|
+
if (on_behalf_of_operator_id) body.on_behalf_of_operator_id = on_behalf_of_operator_id;
|
|
335
|
+
if (Number.isInteger(repair_pr_number) && repair_pr_number > 0) body.repair_pr_number = repair_pr_number;
|
|
336
|
+
if (repair_chain) body.repair_chain = repair_chain;
|
|
337
|
+
const res = await taskReq("POST", "/api/v1/code-task", body);
|
|
338
|
+
if (res.status === 401) {
|
|
339
|
+
cachedFirebaseToken = null;
|
|
340
|
+
throw new Error("enqueue unauthorized (401)");
|
|
341
|
+
}
|
|
342
|
+
if (!res.ok) throw new Error(`enqueue failed: HTTP ${res.status}`);
|
|
343
|
+
const json = await res.json();
|
|
344
|
+
return json && json.task ? json.task : null;
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* Resume a failed/cancelled/max-turn partial code-task. The PR watcher uses
|
|
348
|
+
* this after the runner opens a partial draft PR and CI is no longer pending.
|
|
349
|
+
*/
|
|
350
|
+
async resumeCodeTask(taskId, { automaticRateLimit = false, automaticContinuation = false } = {}) {
|
|
351
|
+
return resumeCodeTaskRequest(taskReq, taskId, { automaticRateLimit, automaticContinuation }, () => {
|
|
352
|
+
cachedFirebaseToken = null;
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
/**
|
|
356
|
+
* Send a CI-green PR through the production verify-before-act merge route.
|
|
357
|
+
* The server inspects the current diff, applies deterministic blockers, runs
|
|
358
|
+
* consensus, records a receipt, and direct-merges only the inspected SHA.
|
|
359
|
+
*/
|
|
360
|
+
async mergeVerifiedPr(prNumber, automationContext) {
|
|
361
|
+
return mergeVerifiedPrRequest(
|
|
362
|
+
req,
|
|
363
|
+
prNumber,
|
|
364
|
+
automationContext,
|
|
365
|
+
() => {
|
|
366
|
+
cachedFirebaseToken = null;
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
/**
|
|
371
|
+
* Append progress / set terminal status. Returns
|
|
372
|
+
* { task } — applied
|
|
373
|
+
* { terminal: true } — task already terminal (operator cancelled): STOP
|
|
374
|
+
*/
|
|
375
|
+
async postProgress(taskId, patch) {
|
|
376
|
+
const progress = {
|
|
377
|
+
...patch,
|
|
378
|
+
...patch.runner_id ? {} : runnerId2 ? { runner_id: runnerId2 } : {},
|
|
379
|
+
...patch.runner_instance_id ? {} : runnerInstanceId ? { runner_instance_id: runnerInstanceId } : {}
|
|
380
|
+
};
|
|
381
|
+
const res = await taskReq("PATCH", `/api/v1/code-task/${taskId}/progress`, progress);
|
|
382
|
+
if (res.status === 409) {
|
|
383
|
+
const conflict = await res.json().catch(() => ({}));
|
|
384
|
+
if (conflict?.error === "code_task_claim_authority_changed") {
|
|
385
|
+
throw new ClaimAuthorityChangedError();
|
|
386
|
+
}
|
|
387
|
+
return { terminal: true };
|
|
388
|
+
}
|
|
389
|
+
if (res.status === 404) return { terminal: true, missing: true };
|
|
390
|
+
if (!res.ok) throw new Error(`progress failed: HTTP ${res.status}`);
|
|
391
|
+
const json = await res.json();
|
|
392
|
+
return { task: json && json.task };
|
|
393
|
+
},
|
|
394
|
+
async getTask(taskId) {
|
|
395
|
+
const res = await taskReq("GET", `/api/v1/code-task/${taskId}`);
|
|
396
|
+
if (res.status === 404) return null;
|
|
397
|
+
if (!res.ok) throw new Error(`getTask failed: HTTP ${res.status}`);
|
|
398
|
+
const json = await res.json();
|
|
399
|
+
return json ? json.task : null;
|
|
400
|
+
},
|
|
401
|
+
async listPrOpenedTasks() {
|
|
402
|
+
return listAllPrOpenedTasks(taskReq);
|
|
403
|
+
},
|
|
404
|
+
async downloadTaskAttachment(taskId, attachmentId) {
|
|
405
|
+
const path2 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
|
|
406
|
+
const res = await taskReq("GET", path2);
|
|
407
|
+
if (res.status === 401) cachedFirebaseToken = null;
|
|
408
|
+
if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
409
|
+
return Buffer.from(await res.arrayBuffer());
|
|
410
|
+
},
|
|
411
|
+
async getTaskKnowledgeContext(taskId, { query } = {}) {
|
|
412
|
+
const body = {};
|
|
413
|
+
if (typeof query === "string" && query.trim()) body.query = query;
|
|
414
|
+
const res = await taskReq("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`, body);
|
|
415
|
+
if (res.status === 401) {
|
|
416
|
+
cachedFirebaseToken = null;
|
|
417
|
+
throw new Error("knowledge-context unauthorized (401)");
|
|
418
|
+
}
|
|
419
|
+
if (res.status === 404) return null;
|
|
420
|
+
if (!res.ok) throw new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
421
|
+
return res.json();
|
|
422
|
+
},
|
|
423
|
+
/**
|
|
424
|
+
* Report this machine's rolling-7-day Claude Code token usage (the real
|
|
425
|
+
* weekly-capacity gauge) PLUS the operator's real Claude weekly % (when
|
|
426
|
+
* available). The daemon authenticates as admin, so the target `operatorId`
|
|
427
|
+
* is named explicitly. Best-effort; throws on a non-2xx so the caller can
|
|
428
|
+
* log + move on.
|
|
429
|
+
*
|
|
430
|
+
* `tokens` = { input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens }.
|
|
431
|
+
* Optional: `claudeWeeklyPct` (number) + `claudeWeeklyResetsAt` (ISO string | null).
|
|
432
|
+
*/
|
|
433
|
+
async postWeeklyTokens({ operatorId, runnerId: runnerId3, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
|
|
434
|
+
const body = {
|
|
435
|
+
operator_id: operatorId,
|
|
436
|
+
runner_id: runnerId3,
|
|
437
|
+
input_tokens: tokens.input_tokens,
|
|
438
|
+
output_tokens: tokens.output_tokens,
|
|
439
|
+
cache_creation_tokens: tokens.cache_creation_tokens,
|
|
440
|
+
cache_read_tokens: tokens.cache_read_tokens
|
|
441
|
+
};
|
|
442
|
+
if (typeof claudeWeeklyPct === "number") {
|
|
443
|
+
body.claude_weekly_pct = claudeWeeklyPct;
|
|
444
|
+
}
|
|
445
|
+
if (claudeWeeklyResetsAt !== void 0) {
|
|
446
|
+
body.claude_weekly_resets_at = claudeWeeklyResetsAt;
|
|
447
|
+
}
|
|
448
|
+
const res = await taskReq("POST", "/api/v1/weekly-tokens", body);
|
|
449
|
+
if (res.status === 401) {
|
|
450
|
+
cachedFirebaseToken = null;
|
|
451
|
+
throw new Error("weekly-tokens unauthorized (401)");
|
|
452
|
+
}
|
|
453
|
+
if (!res.ok) throw new Error(`weekly-tokens failed: HTTP ${res.status}`);
|
|
454
|
+
return true;
|
|
455
|
+
},
|
|
456
|
+
/**
|
|
457
|
+
* Send a liveness heartbeat (M2). The control-plane upserts it under the
|
|
458
|
+
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
459
|
+
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
460
|
+
*/
|
|
461
|
+
async postHeartbeat({ runnerId: runnerId3, runnerInstanceId: runnerInstanceId2, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels }) {
|
|
462
|
+
const body = { runner_id: runnerId3 };
|
|
463
|
+
if (runnerInstanceId2) body.runner_instance_id = runnerInstanceId2;
|
|
464
|
+
if (operatorId) body.operator_id = operatorId;
|
|
465
|
+
if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
|
|
466
|
+
if (typeof activeTasks === "number") body.active_tasks = activeTasks;
|
|
467
|
+
if (typeof maxConcurrency === "number") body.max_concurrency = maxConcurrency;
|
|
468
|
+
if (typeof effectiveConcurrency === "number") body.effective_concurrency = effectiveConcurrency;
|
|
469
|
+
if (typeof measuredTaskSlots === "number") body.measured_task_slots = measuredTaskSlots;
|
|
470
|
+
if (typeof measuredCpuSlots === "number") body.measured_cpu_slots = measuredCpuSlots;
|
|
471
|
+
if (typeof measuredMemorySlots === "number") body.measured_memory_slots = measuredMemorySlots;
|
|
472
|
+
if (version) body.version = version;
|
|
473
|
+
if (daemonVersion) body.daemon_version = daemonVersion;
|
|
474
|
+
if (defaultAgent) body.default_agent = defaultAgent;
|
|
475
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
476
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
477
|
+
if (Array.isArray(supervisorCapabilities) && supervisorCapabilities.length > 0) {
|
|
478
|
+
body.supervisor_capabilities = supervisorCapabilities;
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(servedRepos) && servedRepos.length > 0) body.served_repos = servedRepos;
|
|
481
|
+
if (Array.isArray(servedOperators) && servedOperators.length > 0) {
|
|
482
|
+
body.served_operator_ids = servedOperators;
|
|
483
|
+
}
|
|
484
|
+
if (Array.isArray(availableAgents) && availableAgents.length > 0) {
|
|
485
|
+
body.available_agents = availableAgents;
|
|
486
|
+
}
|
|
487
|
+
if (Array.isArray(accountUsage) && accountUsage.length > 0) {
|
|
488
|
+
body.account_usage = accountUsage;
|
|
489
|
+
}
|
|
490
|
+
if (Array.isArray(availableLocalModels) && availableLocalModels.length > 0) {
|
|
491
|
+
body.available_local_models = availableLocalModels;
|
|
492
|
+
}
|
|
493
|
+
const res = await req("POST", "/api/v1/runner/heartbeat", body, {
|
|
494
|
+
timeoutMs: heartbeatTimeoutMs
|
|
495
|
+
});
|
|
496
|
+
if (res.status === 401) {
|
|
497
|
+
cachedFirebaseToken = null;
|
|
498
|
+
throw new Error("heartbeat unauthorized (401)");
|
|
499
|
+
}
|
|
500
|
+
if (!res.ok) {
|
|
501
|
+
let detail = "";
|
|
502
|
+
try {
|
|
503
|
+
const body2 = await res.json();
|
|
504
|
+
if (Array.isArray(body2?.issue_paths) && body2.issue_paths.length > 0) {
|
|
505
|
+
detail = ` (rejected fields: ${body2.issue_paths.join(", ")})`;
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
throw new Error(`heartbeat failed: HTTP ${res.status}${detail}`);
|
|
510
|
+
}
|
|
511
|
+
return res.json();
|
|
512
|
+
},
|
|
513
|
+
async getRunnerStatus({ operatorId } = {}) {
|
|
514
|
+
const query = operatorId ? `?operator_id=${encodeURIComponent(operatorId)}` : "";
|
|
515
|
+
const res = await req("GET", `/api/v1/runner/status${query}`, void 0, {
|
|
516
|
+
timeoutMs: heartbeatTimeoutMs
|
|
517
|
+
});
|
|
518
|
+
if (res.status === 401) {
|
|
519
|
+
cachedFirebaseToken = null;
|
|
520
|
+
throw new Error("runner status unauthorized (401)");
|
|
521
|
+
}
|
|
522
|
+
if (!res.ok) throw new Error(`runner status failed: HTTP ${res.status}`);
|
|
523
|
+
const body = await res.json();
|
|
524
|
+
return Array.isArray(body?.runners) ? body.runners : [];
|
|
525
|
+
},
|
|
526
|
+
async pollRunnerControl({ runnerId: runnerId3, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities }) {
|
|
527
|
+
const body = { runner_id: runnerId3 };
|
|
528
|
+
if (operatorId) body.operator_id = operatorId;
|
|
529
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
530
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
531
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
532
|
+
const res = await taskReq("POST", "/api/v1/runner/control/poll", body);
|
|
533
|
+
if (res.status === 401) {
|
|
534
|
+
cachedFirebaseToken = null;
|
|
535
|
+
throw new Error("runner control poll unauthorized (401)");
|
|
536
|
+
}
|
|
537
|
+
if (!res.ok) throw new Error(`runner control poll failed: HTTP ${res.status}`);
|
|
538
|
+
const json = await res.json();
|
|
539
|
+
const action = json?.action;
|
|
540
|
+
return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
|
|
541
|
+
},
|
|
542
|
+
async completeRunnerControl(actionId, { runnerId: runnerId3, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities, status, detail }) {
|
|
543
|
+
const body = { runner_id: runnerId3, status };
|
|
544
|
+
if (operatorId) body.operator_id = operatorId;
|
|
545
|
+
if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
|
|
546
|
+
if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
|
|
547
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
548
|
+
if (detail) body.detail = detail;
|
|
549
|
+
const res = await taskReq("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
|
|
550
|
+
if (res.status === 401) {
|
|
551
|
+
cachedFirebaseToken = null;
|
|
552
|
+
throw new Error("runner control completion unauthorized (401)");
|
|
553
|
+
}
|
|
554
|
+
if (!res.ok) throw new Error(`runner control completion failed: HTTP ${res.status}`);
|
|
555
|
+
const json = await res.json();
|
|
556
|
+
return json?.action || null;
|
|
557
|
+
},
|
|
558
|
+
/** Mint a GitHub App installation token — see installation-token.mjs. */
|
|
559
|
+
async getInstallationToken({ required = false, readOnly = false, repo = null } = {}) {
|
|
560
|
+
return fetchInstallationToken({ req: taskReq, required, readOnly, repo });
|
|
561
|
+
},
|
|
562
|
+
/**
|
|
563
|
+
* Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
|
|
564
|
+
* Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'marathon'; 'ultracode' legacy),
|
|
565
|
+
* defaulting to 'standard' on any error. Never throws — best-effort.
|
|
566
|
+
*/
|
|
567
|
+
async getDispatchMode() {
|
|
568
|
+
try {
|
|
569
|
+
const res = await taskReq("GET", "/api/v1/dispatch-mode-config");
|
|
570
|
+
if (!res.ok) return "standard";
|
|
571
|
+
const json = await res.json();
|
|
572
|
+
return json?.dispatchMode || "standard";
|
|
573
|
+
} catch {
|
|
574
|
+
return "standard";
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs
|
|
581
|
+
import { spawnSync } from "node:child_process";
|
|
582
|
+
import { existsSync } from "node:fs";
|
|
583
|
+
import { win32 } from "node:path";
|
|
584
|
+
var DEFAULT_RUNNER_PACKAGE = "@algosuite/vo-mcp@beta";
|
|
585
|
+
var PACKAGE_SPEC_RE = /^@algosuite\/vo-mcp@(beta|latest|\d+(?:\.\d+){0,2}(?:-[\w.-]+)?)$/u;
|
|
586
|
+
var MAX_DIAGNOSTIC_CHARS = 800;
|
|
587
|
+
var NPM_CLI_SUFFIX = `\\${win32.join("node_modules", "npm", "bin", "npm-cli.js").toLowerCase()}`;
|
|
588
|
+
function escapeRegExp(value) {
|
|
589
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
590
|
+
}
|
|
591
|
+
function sanitizeMaintenanceDiagnostic(raw, env = {}) {
|
|
592
|
+
let value = String(raw || "").replace(/\b(?:vocred|npm|gh[oprsu])_[A-Za-z0-9._-]+\b/gu, "[REDACTED]").replace(/\bBearer\s+\S+/giu, "Bearer [REDACTED]").replace(/\b(_?authToken|token|password|secret|credential)(\s*[=:]\s*)\S+/giu, "$1$2[REDACTED]");
|
|
593
|
+
for (const [name, secret] of Object.entries(env)) {
|
|
594
|
+
if (!/(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)/iu.test(name)) continue;
|
|
595
|
+
const text = String(secret || "");
|
|
596
|
+
if (text.length < 4) continue;
|
|
597
|
+
value = value.replace(new RegExp(escapeRegExp(text), "gu"), "[REDACTED]");
|
|
598
|
+
}
|
|
599
|
+
value = value.replace(/\s+/gu, " ").trim();
|
|
600
|
+
if (value.length <= MAX_DIAGNOSTIC_CHARS) return value;
|
|
601
|
+
return `${value.slice(0, MAX_DIAGNOSTIC_CHARS - 1)}\u2026`;
|
|
602
|
+
}
|
|
603
|
+
function isNpmCliPath(value) {
|
|
604
|
+
return typeof value === "string" && win32.isAbsolute(value) && win32.normalize(value).toLowerCase().endsWith(NPM_CLI_SUFFIX);
|
|
605
|
+
}
|
|
606
|
+
function resolveNpmCli({
|
|
607
|
+
env = process.env,
|
|
608
|
+
execPath = process.execPath,
|
|
609
|
+
fileExists = existsSync
|
|
610
|
+
} = {}) {
|
|
611
|
+
const candidates = [];
|
|
612
|
+
if (isNpmCliPath(env.npm_execpath)) candidates.push(env.npm_execpath);
|
|
613
|
+
candidates.push(win32.join(win32.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"));
|
|
614
|
+
const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
|
|
615
|
+
for (const entry of pathValue.split(";")) {
|
|
616
|
+
const trimmed = entry.trim();
|
|
617
|
+
if (!win32.isAbsolute(trimmed)) continue;
|
|
618
|
+
candidates.push(win32.join(trimmed, "node_modules", "npm", "bin", "npm-cli.js"));
|
|
619
|
+
}
|
|
620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
621
|
+
for (const candidate of candidates) {
|
|
622
|
+
const normalized = win32.normalize(candidate);
|
|
623
|
+
const key = normalized.toLowerCase();
|
|
624
|
+
if (seen.has(key) || !isNpmCliPath(normalized)) continue;
|
|
625
|
+
seen.add(key);
|
|
626
|
+
if (fileExists(normalized)) return normalized;
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
function buildMaintenanceCommand(kind, {
|
|
631
|
+
platform = process.platform,
|
|
632
|
+
packageSpec = DEFAULT_RUNNER_PACKAGE,
|
|
633
|
+
env = process.env,
|
|
634
|
+
execPath = process.execPath,
|
|
635
|
+
fileExists = existsSync
|
|
636
|
+
} = {}) {
|
|
637
|
+
if (!["update", "reinstall"].includes(kind)) return null;
|
|
638
|
+
if (!PACKAGE_SPEC_RE.test(packageSpec)) throw new Error("unsafe runner package spec");
|
|
639
|
+
const npmCli = platform === "win32" ? resolveNpmCli({ env, execPath, fileExists }) : null;
|
|
640
|
+
if (platform === "win32" && !npmCli) throw new Error("trusted npm CLI not found");
|
|
641
|
+
const command = platform === "win32" ? execPath : "npm";
|
|
642
|
+
const args = [...npmCli ? [npmCli] : [], "install", "-g", packageSpec];
|
|
643
|
+
if (kind === "reinstall") args.push("--force");
|
|
644
|
+
return { command, args };
|
|
645
|
+
}
|
|
646
|
+
function runHostMaintenance(kind, {
|
|
647
|
+
platform = process.platform,
|
|
648
|
+
packageSpec = DEFAULT_RUNNER_PACKAGE,
|
|
649
|
+
env = process.env,
|
|
650
|
+
execPath = process.execPath,
|
|
651
|
+
fileExists = existsSync,
|
|
652
|
+
spawn: spawn2 = spawnSync,
|
|
653
|
+
log = () => {
|
|
654
|
+
}
|
|
655
|
+
} = {}) {
|
|
656
|
+
if (kind === "reconnect") return { ok: true, status: 0, command: null, args: [] };
|
|
657
|
+
let command;
|
|
658
|
+
try {
|
|
659
|
+
command = buildMaintenanceCommand(kind, { platform, packageSpec, env, execPath, fileExists });
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
status: 2,
|
|
664
|
+
command: null,
|
|
665
|
+
args: [],
|
|
666
|
+
detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (!command) return { ok: false, status: 2, command: null, args: [] };
|
|
670
|
+
log(`runner maintenance: ${command.command} ${command.args.join(" ")}`);
|
|
671
|
+
const result = spawn2(command.command, command.args, {
|
|
672
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
673
|
+
encoding: "utf8",
|
|
674
|
+
env,
|
|
675
|
+
shell: false,
|
|
676
|
+
windowsHide: true
|
|
677
|
+
});
|
|
678
|
+
const status = typeof result.status === "number" ? result.status : 1;
|
|
679
|
+
const detail = sanitizeMaintenanceDiagnostic(
|
|
680
|
+
[result.stderr, result.stdout, result.error?.message].filter(Boolean).join("\n"),
|
|
681
|
+
env
|
|
682
|
+
);
|
|
683
|
+
if (detail) log(`runner maintenance result: ${detail}`);
|
|
684
|
+
return { ok: status === 0, status, command: command.command, args: command.args, detail };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/runner/bundled-runtime-updater.mjs
|
|
688
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
689
|
+
import {
|
|
690
|
+
existsSync as existsSync4,
|
|
691
|
+
lstatSync as lstatSync3,
|
|
692
|
+
mkdirSync as mkdirSync2,
|
|
693
|
+
readFileSync as readFileSync4,
|
|
694
|
+
readdirSync as readdirSync3,
|
|
695
|
+
renameSync as renameSync2,
|
|
696
|
+
rmSync as rmSync2,
|
|
697
|
+
writeFileSync as writeFileSync2
|
|
698
|
+
} from "node:fs";
|
|
699
|
+
import { basename, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
700
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
701
|
+
|
|
702
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-authorization.mjs
|
|
703
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
704
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
705
|
+
import { dirname, posix, resolve as resolve2 } from "node:path";
|
|
706
|
+
import { fileURLToPath } from "node:url";
|
|
707
|
+
|
|
708
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-staged-tree.mjs
|
|
709
|
+
import { execFileSync } from "node:child_process";
|
|
710
|
+
import { createHash } from "node:crypto";
|
|
711
|
+
import {
|
|
712
|
+
existsSync as existsSync2,
|
|
713
|
+
lstatSync,
|
|
714
|
+
readFileSync,
|
|
715
|
+
readdirSync
|
|
716
|
+
} from "node:fs";
|
|
717
|
+
import { isAbsolute, join, relative, resolve, sep, win32 as win322 } from "node:path";
|
|
718
|
+
var WINDOWS_REPARSE_ATTRIBUTE = 1024;
|
|
719
|
+
var RUNNER_LOCK_KEY = "node_modules/@algosuite/vo-mcp";
|
|
720
|
+
var INSTALLED_TREE_ALGORITHM = "algohq-node-modules-manifest-sha256-v1";
|
|
721
|
+
function canonical(value) {
|
|
722
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
723
|
+
if (value && typeof value === "object") {
|
|
724
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
725
|
+
}
|
|
726
|
+
return value;
|
|
727
|
+
}
|
|
728
|
+
function canonicalJson(value) {
|
|
729
|
+
return JSON.stringify(canonical(value));
|
|
730
|
+
}
|
|
731
|
+
function hasFileAttribute(value, bit) {
|
|
732
|
+
if (typeof value === "bigint") return (value & BigInt(bit)) !== 0n;
|
|
733
|
+
return Number.isSafeInteger(value) && (value & bit) !== 0;
|
|
734
|
+
}
|
|
735
|
+
function isReparseStat(stats) {
|
|
736
|
+
if (!stats || typeof stats !== "object") return false;
|
|
737
|
+
if (typeof stats.isSymbolicLink === "function" && stats.isSymbolicLink()) return true;
|
|
738
|
+
if (stats.reparseTag !== void 0 && stats.reparseTag !== null && stats.reparseTag !== 0) return true;
|
|
739
|
+
return ["fileAttributes", "fileAttribute", "attributes"].some((key) => hasFileAttribute(stats[key], WINDOWS_REPARSE_ATTRIBUTE));
|
|
740
|
+
}
|
|
741
|
+
function platformConstraintAllows(values, target) {
|
|
742
|
+
if (!Array.isArray(values) || values.length === 0) return true;
|
|
743
|
+
if (values.some((value) => value === `!${target}`)) return false;
|
|
744
|
+
const positive = values.filter((value) => typeof value === "string" && !value.startsWith("!"));
|
|
745
|
+
return positive.length === 0 || positive.includes(target);
|
|
746
|
+
}
|
|
747
|
+
function isOmittedOptionalPackage(entry, platform = { os: "win32", arch: "x64" }) {
|
|
748
|
+
return entry?.optional === true && (!platformConstraintAllows(entry.os, platform.os) || !platformConstraintAllows(entry.cpu, platform.arch));
|
|
749
|
+
}
|
|
750
|
+
function assertContained(root, candidate, label) {
|
|
751
|
+
const rel = relative(root, candidate);
|
|
752
|
+
if (rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)) return;
|
|
753
|
+
throw new Error(`staged runtime ${label} escapes the payload root`);
|
|
754
|
+
}
|
|
755
|
+
function normalizeReportedPath(root, candidate) {
|
|
756
|
+
const absolute = resolve(String(candidate || ""));
|
|
757
|
+
assertContained(root, absolute, "reparse point");
|
|
758
|
+
return absolute;
|
|
759
|
+
}
|
|
760
|
+
function listWindowsReparsePoints(root, {
|
|
761
|
+
execFile = execFileSync,
|
|
762
|
+
env = process.env,
|
|
763
|
+
platform = process.platform
|
|
764
|
+
} = {}) {
|
|
765
|
+
if (platform !== "win32") return [];
|
|
766
|
+
const systemRoot = String(env.SystemRoot || env.SYSTEMROOT || "");
|
|
767
|
+
if (!win322.isAbsolute(systemRoot) || win322.normalize(systemRoot) !== systemRoot) {
|
|
768
|
+
throw new Error("staged runtime trusted PowerShell root unavailable");
|
|
769
|
+
}
|
|
770
|
+
const system32 = win322.join(systemRoot, "System32");
|
|
771
|
+
const powershell = win322.join(system32, "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
772
|
+
const script = [
|
|
773
|
+
'$ErrorActionPreference = "Stop"',
|
|
774
|
+
"$root = [IO.Path]::GetFullPath($env:ALGOHQ_REPARSE_ROOT)",
|
|
775
|
+
"$items = @((Get-Item -LiteralPath $root -Force)) + @(Get-ChildItem -LiteralPath $root -Force -Recurse)",
|
|
776
|
+
"foreach ($item in $items) {",
|
|
777
|
+
" if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {",
|
|
778
|
+
" [Console]::Out.WriteLine($item.FullName)",
|
|
779
|
+
" }",
|
|
780
|
+
"}"
|
|
781
|
+
].join("; ");
|
|
782
|
+
const output = execFile(powershell, [
|
|
783
|
+
"-NoLogo",
|
|
784
|
+
"-NoProfile",
|
|
785
|
+
"-NonInteractive",
|
|
786
|
+
"-Command",
|
|
787
|
+
script
|
|
788
|
+
], {
|
|
789
|
+
cwd: system32,
|
|
790
|
+
encoding: "utf8",
|
|
791
|
+
env: { SystemRoot: systemRoot, ALGOHQ_REPARSE_ROOT: win322.resolve(root) },
|
|
792
|
+
windowsHide: true
|
|
793
|
+
});
|
|
794
|
+
return String(output || "").split(/\r?\n/u).filter(Boolean).map((item) => normalizeReportedPath(root, item));
|
|
795
|
+
}
|
|
796
|
+
function normalizedRunnerRecord(actual, expected) {
|
|
797
|
+
const normalized = structuredClone(actual);
|
|
798
|
+
if (normalized?.resolved === expected?.resolved) return normalized;
|
|
799
|
+
if (typeof normalized?.resolved !== "string" || !normalized.resolved.startsWith("file:")) {
|
|
800
|
+
return normalized;
|
|
801
|
+
}
|
|
802
|
+
const fileName = normalized.resolved.slice("file:".length).replaceAll("\\", "/").split("/").at(-1);
|
|
803
|
+
const expectedFileNames = /* @__PURE__ */ new Set([
|
|
804
|
+
String(expected.resolved || "").split("/").at(-1),
|
|
805
|
+
`algosuite-vo-mcp-${expected.version}.tgz`
|
|
806
|
+
]);
|
|
807
|
+
if (expectedFileNames.has(fileName)) normalized.resolved = expected.resolved;
|
|
808
|
+
return normalized;
|
|
809
|
+
}
|
|
810
|
+
function validateLock(lock, authorization) {
|
|
811
|
+
if (!lock || typeof lock !== "object" || Array.isArray(lock)) {
|
|
812
|
+
throw new Error("staged runtime package-lock must be an object");
|
|
813
|
+
}
|
|
814
|
+
if (lock.lockfileVersion !== authorization.dependency_lock.source_lockfile_version) {
|
|
815
|
+
throw new Error("staged runtime package-lock version mismatch");
|
|
816
|
+
}
|
|
817
|
+
if (!lock.packages || typeof lock.packages !== "object" || Array.isArray(lock.packages)) {
|
|
818
|
+
throw new Error("staged runtime package-lock package map missing");
|
|
819
|
+
}
|
|
820
|
+
const expected = authorization.dependency_lock.packages;
|
|
821
|
+
const actual = Object.fromEntries(Object.entries(lock.packages).filter(([key]) => key !== ""));
|
|
822
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
823
|
+
const actualKeys = Object.keys(actual).sort();
|
|
824
|
+
if (canonicalJson(actualKeys) !== canonicalJson(expectedKeys)) {
|
|
825
|
+
throw new Error("staged runtime package-lock package set mismatch");
|
|
826
|
+
}
|
|
827
|
+
for (const key of expectedKeys) {
|
|
828
|
+
const record = key === RUNNER_LOCK_KEY ? normalizedRunnerRecord(actual[key], expected[key]) : actual[key];
|
|
829
|
+
if (canonicalJson(record) !== canonicalJson(expected[key])) {
|
|
830
|
+
throw new Error(`staged runtime package-lock record mismatch: ${key}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return { actual, expected };
|
|
834
|
+
}
|
|
835
|
+
function checkPathKind(path2, expectedKind, fsOps, label) {
|
|
836
|
+
if (!fsOps.exists(path2)) throw new Error(`staged runtime ${label} missing`);
|
|
837
|
+
const stats = fsOps.lstat(path2);
|
|
838
|
+
if (isReparseStat(stats) || fsOps.isReparsePoint(path2, stats)) {
|
|
839
|
+
throw new Error(`staged runtime ${label} is a reparse point`);
|
|
840
|
+
}
|
|
841
|
+
if (expectedKind === "directory" && !stats.isDirectory()) {
|
|
842
|
+
throw new Error(`staged runtime ${label} is not a directory`);
|
|
843
|
+
}
|
|
844
|
+
if (expectedKind === "file" && !stats.isFile()) {
|
|
845
|
+
throw new Error(`staged runtime ${label} is not a regular file`);
|
|
846
|
+
}
|
|
847
|
+
if (expectedKind === "file" && Number(stats.nlink) > 1) {
|
|
848
|
+
throw new Error(`staged runtime ${label} is hardlinked`);
|
|
849
|
+
}
|
|
850
|
+
return stats;
|
|
851
|
+
}
|
|
852
|
+
function verifyInstalledPackages(payloadRoot, packageRecords, platform, fsOps) {
|
|
853
|
+
const omitted = [];
|
|
854
|
+
let installed = 0;
|
|
855
|
+
for (const [key, entry] of Object.entries(packageRecords)) {
|
|
856
|
+
const path2 = resolve(payloadRoot, key);
|
|
857
|
+
assertContained(payloadRoot, path2, "package path");
|
|
858
|
+
const shouldOmit = isOmittedOptionalPackage(entry, platform);
|
|
859
|
+
if (shouldOmit) {
|
|
860
|
+
omitted.push(key);
|
|
861
|
+
if (fsOps.exists(path2)) throw new Error(`staged runtime optional package should be omitted: ${key}`);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
checkPathKind(path2, "directory", fsOps, `installed package ${key}`);
|
|
865
|
+
installed += 1;
|
|
866
|
+
}
|
|
867
|
+
return { installed, omitted: omitted.sort() };
|
|
868
|
+
}
|
|
869
|
+
function treeRecord(kind, path2, stats, fileHash = "") {
|
|
870
|
+
if (kind === "d") return `d ${path2}\r
|
|
871
|
+
`;
|
|
872
|
+
return `f ${path2} ${stats.size} ${fileHash}\r
|
|
873
|
+
`;
|
|
874
|
+
}
|
|
875
|
+
function computeInstalledTree(nodeModulesRoot, fsOps = {}) {
|
|
876
|
+
const ops = {
|
|
877
|
+
exists: existsSync2,
|
|
878
|
+
lstat: lstatSync,
|
|
879
|
+
readdir: (path2) => readdirSync(path2, { withFileTypes: true }),
|
|
880
|
+
readFile: readFileSync,
|
|
881
|
+
isReparsePoint: () => false,
|
|
882
|
+
listReparsePoints: listWindowsReparsePoints,
|
|
883
|
+
...fsOps
|
|
884
|
+
};
|
|
885
|
+
const root = resolve(nodeModulesRoot);
|
|
886
|
+
checkPathKind(root, "directory", ops, "node_modules root");
|
|
887
|
+
const reported = ops.listReparsePoints(root);
|
|
888
|
+
if (!Array.isArray(reported)) throw new Error("staged runtime reparse probe returned an invalid result");
|
|
889
|
+
if (reported.length > 0) throw new Error("staged runtime tree contains a Windows reparse point");
|
|
890
|
+
let fileCount = 0;
|
|
891
|
+
let directoryCount = 1;
|
|
892
|
+
let byteCount = 0;
|
|
893
|
+
const entries = [];
|
|
894
|
+
function walk(absolute, relativePath) {
|
|
895
|
+
for (const entry of ops.readdir(absolute)) {
|
|
896
|
+
const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
897
|
+
if (childRelative === ".package-lock.json") continue;
|
|
898
|
+
const child = resolve(absolute, entry.name);
|
|
899
|
+
assertContained(root, child, "tree entry");
|
|
900
|
+
const stats = ops.lstat(child);
|
|
901
|
+
if (isReparseStat(stats) || ops.isReparsePoint(child, stats)) {
|
|
902
|
+
throw new Error(`staged runtime tree contains a reparse point: ${childRelative}`);
|
|
903
|
+
}
|
|
904
|
+
if (stats.isDirectory()) {
|
|
905
|
+
directoryCount += 1;
|
|
906
|
+
entries.push({ kind: "d", path: childRelative, stats });
|
|
907
|
+
walk(child, childRelative);
|
|
908
|
+
} else if (stats.isFile()) {
|
|
909
|
+
if (Number(stats.nlink) > 1) {
|
|
910
|
+
throw new Error(`staged runtime tree contains a hardlinked file: ${childRelative}`);
|
|
911
|
+
}
|
|
912
|
+
fileCount += 1;
|
|
913
|
+
byteCount += Number(stats.size);
|
|
914
|
+
const digest = createHash("sha256").update(ops.readFile(child)).digest("hex");
|
|
915
|
+
entries.push({ kind: "f", path: childRelative, stats, digest });
|
|
916
|
+
} else {
|
|
917
|
+
throw new Error(`staged runtime tree contains a non-file entry: ${childRelative}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
walk(root, "");
|
|
922
|
+
entries.sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
|
|
923
|
+
const manifest = treeRecord("d", "", { size: 0 }) + entries.map((entry) => treeRecord(entry.kind, entry.path, entry.stats, entry.digest)).join("");
|
|
924
|
+
return {
|
|
925
|
+
algorithm: INSTALLED_TREE_ALGORITHM,
|
|
926
|
+
sha256: createHash("sha256").update(manifest, "utf8").digest("hex"),
|
|
927
|
+
file_count: fileCount,
|
|
928
|
+
directory_count: directoryCount,
|
|
929
|
+
byte_count: byteCount,
|
|
930
|
+
canonical_manifest_byte_count: Buffer.byteLength(manifest),
|
|
931
|
+
reparse_point_count: 0,
|
|
932
|
+
hardlinked_file_count: 0
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function compareStagedRuntime({
|
|
936
|
+
payloadRoot,
|
|
937
|
+
nodeModulesRoot = join(payloadRoot, "node_modules"),
|
|
938
|
+
packageLockFile = join(payloadRoot, "package-lock.json"),
|
|
939
|
+
authorization,
|
|
940
|
+
fsOps = {}
|
|
941
|
+
}) {
|
|
942
|
+
const payload = resolve(payloadRoot);
|
|
943
|
+
const modules = resolve(nodeModulesRoot);
|
|
944
|
+
const lockFile = resolve(packageLockFile);
|
|
945
|
+
if (modules !== resolve(payload, "node_modules") || lockFile !== resolve(payload, "package-lock.json")) {
|
|
946
|
+
throw new Error("staged runtime paths do not identify one canonical payload");
|
|
947
|
+
}
|
|
948
|
+
const ops = {
|
|
949
|
+
exists: existsSync2,
|
|
950
|
+
lstat: lstatSync,
|
|
951
|
+
readFile: readFileSync,
|
|
952
|
+
isReparsePoint: () => false,
|
|
953
|
+
...fsOps
|
|
954
|
+
};
|
|
955
|
+
checkPathKind(lockFile, "file", ops, "package-lock");
|
|
956
|
+
const lock = JSON.parse(ops.readFile(lockFile, "utf8"));
|
|
957
|
+
const { expected } = validateLock(lock, authorization);
|
|
958
|
+
const platform = { os: authorization.platform.os, arch: authorization.platform.arch };
|
|
959
|
+
const packages = verifyInstalledPackages(payload, expected, platform, ops);
|
|
960
|
+
const declaredOmitted = [...authorization.dependency_lock.windows_x64_omitted_optional_entries].sort();
|
|
961
|
+
if (canonicalJson(packages.omitted) !== canonicalJson(declaredOmitted)) {
|
|
962
|
+
throw new Error("staged runtime optional omission list mismatch");
|
|
963
|
+
}
|
|
964
|
+
if (packages.installed !== Object.keys(expected).length - packages.omitted.length) {
|
|
965
|
+
throw new Error("staged runtime installed package count mismatch");
|
|
966
|
+
}
|
|
967
|
+
const tree = computeInstalledTree(modules, fsOps);
|
|
968
|
+
if (canonicalJson(tree) !== canonicalJson(authorization.installed_tree)) {
|
|
969
|
+
throw new Error("staged runtime installed tree mismatch");
|
|
970
|
+
}
|
|
971
|
+
return { ok: true, packageCount: Object.keys(expected).length, ...packages, tree };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ../../scripts/virtual-office/runner-bootstrap/runtime-authorization.mjs
|
|
975
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
976
|
+
var BETA13_WINDOWS_X64_AUTHORIZATION = resolve2(
|
|
977
|
+
HERE,
|
|
978
|
+
"authorizations",
|
|
979
|
+
"vo-mcp-0.2.0-beta.13-win32-x64.json"
|
|
980
|
+
);
|
|
981
|
+
var EXPECTED = Object.freeze({
|
|
982
|
+
authorizationId: "vo-mcp-0.2.0-beta.13-win32-x64-v1",
|
|
983
|
+
packageName: "@algosuite/vo-mcp",
|
|
984
|
+
version: "0.2.0-beta.13",
|
|
985
|
+
registry: "https://registry.npmjs.org/",
|
|
986
|
+
tarballUrl: "https://registry.npmjs.org/@algosuite/vo-mcp/-/vo-mcp-0.2.0-beta.13.tgz",
|
|
987
|
+
integrity: "sha512-TQa5VaEFsaleHbAZZp+zS4u5U/0zQBIGOiPDGn9IqfZfdMltH2dY81nTftvu5EUABthE1xTCrdSzJjO9mzkNag==",
|
|
988
|
+
tarballSha256: "c1e39e8bb2df7f53f48e46e77ffb617452a01849469ff4757b4384a478c1cbff",
|
|
989
|
+
npmShasumSha1: "f076649b31294aa38deb7852f38a889e0d0fbe44",
|
|
990
|
+
gitHead: "ba00db90720416fa7474581eb823c6255221880f",
|
|
991
|
+
sourceLockSha256: "f8bbb5e81a0057ee2d60145580ec07f7e7055d999bdd038515c6c4e88af6cc92",
|
|
992
|
+
canonicalEntriesSha256: "7f6b2f93ccb93ad625ed244dc59ac556792e478178fc690ad2ca2c6f3a2325d2",
|
|
993
|
+
entryCount: 107,
|
|
994
|
+
optionalEntryCount: 13,
|
|
995
|
+
installedEntryCount: 96,
|
|
996
|
+
treeAlgorithm: "algohq-node-modules-manifest-sha256-v1",
|
|
997
|
+
treeSha256: "9c8b0749d7ae1e2c132ef08c5b5655673ae88432859c561806276c9eb18f4874",
|
|
998
|
+
treeFileCount: 3538,
|
|
999
|
+
treeDirectoryCount: 553,
|
|
1000
|
+
treeByteCount: 20566445,
|
|
1001
|
+
treeManifestByteCount: 412062
|
|
1002
|
+
});
|
|
1003
|
+
var SRI_RE = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
1004
|
+
var SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
1005
|
+
function canonical2(value) {
|
|
1006
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
1007
|
+
if (value && typeof value === "object") {
|
|
1008
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical2(value[key])]));
|
|
1009
|
+
}
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
function sha256Canonical(value) {
|
|
1013
|
+
return createHash2("sha256").update(`${JSON.stringify(canonical2(value))}
|
|
1014
|
+
`, "utf8").digest("hex");
|
|
1015
|
+
}
|
|
1016
|
+
function assertEqual(actual, expected, label) {
|
|
1017
|
+
if (actual !== expected) throw new Error(`runtime authorization ${label} mismatch`);
|
|
1018
|
+
}
|
|
1019
|
+
function isOmittedOnWindowsX64(entry) {
|
|
1020
|
+
return isOmittedOptionalPackage(entry, { os: "win32", arch: "x64" });
|
|
1021
|
+
}
|
|
1022
|
+
function validateRuntimeAuthorization(value) {
|
|
1023
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1024
|
+
throw new Error("runtime authorization must be an object");
|
|
1025
|
+
}
|
|
1026
|
+
assertEqual(value.schema_version, 1, "schema version");
|
|
1027
|
+
assertEqual(value.authorization_id, EXPECTED.authorizationId, "id");
|
|
1028
|
+
assertEqual(value.package?.name, EXPECTED.packageName, "package name");
|
|
1029
|
+
assertEqual(value.package?.version, EXPECTED.version, "package version");
|
|
1030
|
+
assertEqual(value.package?.registry, EXPECTED.registry, "registry");
|
|
1031
|
+
assertEqual(value.package?.tarball_url, EXPECTED.tarballUrl, "tarball URL");
|
|
1032
|
+
assertEqual(value.package?.sri_sha512, EXPECTED.integrity, "package integrity");
|
|
1033
|
+
assertEqual(value.package?.tarball_sha256, EXPECTED.tarballSha256, "tarball sha256");
|
|
1034
|
+
assertEqual(value.package?.npm_shasum_sha1, EXPECTED.npmShasumSha1, "npm shasum");
|
|
1035
|
+
assertEqual(value.package?.git_head, EXPECTED.gitHead, "git head");
|
|
1036
|
+
assertEqual(value.package?.packed_file_count, 25, "packed file count");
|
|
1037
|
+
assertEqual(value.package?.packed_bytes, 846151, "packed byte count");
|
|
1038
|
+
assertEqual(value.package?.unpacked_bytes, 3350387, "unpacked byte count");
|
|
1039
|
+
assertEqual(value.platform?.os, "win32", "operating system");
|
|
1040
|
+
assertEqual(value.platform?.arch, "x64", "architecture");
|
|
1041
|
+
assertEqual(value.platform?.package_node_engine, ">=22.5.0", "package node engine");
|
|
1042
|
+
if (!/^24\.15\.0$/u.test(String(value.platform?.authorization_builder_node || "")) || value.platform?.authorization_builder_npm !== "11.12.1") {
|
|
1043
|
+
throw new Error("runtime authorization builder toolchain mismatch");
|
|
1044
|
+
}
|
|
1045
|
+
for (const [key, expected] of Object.entries({
|
|
1046
|
+
ignore_scripts: true,
|
|
1047
|
+
bin_links: false,
|
|
1048
|
+
include_optional: true,
|
|
1049
|
+
omit_dev: true,
|
|
1050
|
+
audit: false,
|
|
1051
|
+
fund: false,
|
|
1052
|
+
reject_links_reparse_points: true,
|
|
1053
|
+
reject_hardlinks: true,
|
|
1054
|
+
remove_generated_node_modules_package_lock_before_tree_validation: true
|
|
1055
|
+
})) assertEqual(value.install_contract?.[key], expected, `install contract ${key}`);
|
|
1056
|
+
assertEqual(value.install_contract?.allowed_registry_prefix, EXPECTED.registry, "registry prefix");
|
|
1057
|
+
const lock = value.dependency_lock;
|
|
1058
|
+
if (!lock?.packages || typeof lock.packages !== "object" || Array.isArray(lock.packages)) {
|
|
1059
|
+
throw new Error("runtime authorization dependency set missing");
|
|
1060
|
+
}
|
|
1061
|
+
assertEqual(lock.source_lockfile_version, 3, "lockfile version");
|
|
1062
|
+
assertEqual(lock.source_lock_sha256, EXPECTED.sourceLockSha256, "source lock sha256");
|
|
1063
|
+
assertEqual(lock.canonical_entries_sha256, EXPECTED.canonicalEntriesSha256, "entry-set sha256");
|
|
1064
|
+
assertEqual(lock.entry_count, EXPECTED.entryCount, "entry count");
|
|
1065
|
+
assertEqual(lock.integrity_entry_count, EXPECTED.entryCount, "integrity count");
|
|
1066
|
+
assertEqual(lock.optional_entry_count, EXPECTED.optionalEntryCount, "optional count");
|
|
1067
|
+
assertEqual(lock.windows_x64_installed_entry_count, EXPECTED.installedEntryCount, "installed count");
|
|
1068
|
+
const entries = Object.entries(lock.packages);
|
|
1069
|
+
assertEqual(entries.length, EXPECTED.entryCount, "package map count");
|
|
1070
|
+
for (const [key, entry] of entries) {
|
|
1071
|
+
if (!key.startsWith("node_modules/") || key.includes("\\") || posix.normalize(key) !== key || key.split("/").includes("..")) {
|
|
1072
|
+
throw new Error(`runtime authorization has unsafe package path: ${key}`);
|
|
1073
|
+
}
|
|
1074
|
+
if (!entry || typeof entry !== "object" || entry.link === true) {
|
|
1075
|
+
throw new Error(`runtime authorization contains a linked package: ${key}`);
|
|
1076
|
+
}
|
|
1077
|
+
if (!SRI_RE.test(String(entry.integrity || ""))) {
|
|
1078
|
+
throw new Error(`runtime authorization package lacks sha512 integrity: ${key}`);
|
|
1079
|
+
}
|
|
1080
|
+
if (!String(entry.resolved || "").startsWith(EXPECTED.registry)) {
|
|
1081
|
+
throw new Error(`runtime authorization package is outside the public registry: ${key}`);
|
|
1082
|
+
}
|
|
1083
|
+
if (entry.hasInstallScript === true) {
|
|
1084
|
+
throw new Error(`runtime authorization package declares an install script: ${key}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const runner = lock.packages["node_modules/@algosuite/vo-mcp"];
|
|
1088
|
+
assertEqual(runner?.version, EXPECTED.version, "runner dependency version");
|
|
1089
|
+
assertEqual(runner?.integrity, EXPECTED.integrity, "runner dependency integrity");
|
|
1090
|
+
assertEqual(sha256Canonical(lock.packages), EXPECTED.canonicalEntriesSha256, "computed entry-set sha256");
|
|
1091
|
+
const omitted = entries.filter(([, entry]) => isOmittedOnWindowsX64(entry)).map(([key]) => key).sort();
|
|
1092
|
+
const declaredOmitted = [...lock.windows_x64_omitted_optional_entries || []].sort();
|
|
1093
|
+
assertEqual(JSON.stringify(declaredOmitted), JSON.stringify(omitted), "omitted optional entries");
|
|
1094
|
+
assertEqual(entries.length - omitted.length, EXPECTED.installedEntryCount, "derived installed count");
|
|
1095
|
+
const tree = value.installed_tree;
|
|
1096
|
+
assertEqual(tree?.algorithm, EXPECTED.treeAlgorithm, "tree algorithm");
|
|
1097
|
+
if (!SHA256_RE.test(String(tree?.sha256 || ""))) throw new Error("runtime authorization tree hash invalid");
|
|
1098
|
+
assertEqual(tree.sha256, EXPECTED.treeSha256, "tree sha256");
|
|
1099
|
+
assertEqual(tree.file_count, EXPECTED.treeFileCount, "tree file count");
|
|
1100
|
+
assertEqual(tree.directory_count, EXPECTED.treeDirectoryCount, "tree directory count");
|
|
1101
|
+
assertEqual(tree.byte_count, EXPECTED.treeByteCount, "tree byte count");
|
|
1102
|
+
assertEqual(tree.canonical_manifest_byte_count, EXPECTED.treeManifestByteCount, "tree manifest byte count");
|
|
1103
|
+
assertEqual(tree.reparse_point_count, 0, "tree reparse count");
|
|
1104
|
+
assertEqual(tree.hardlinked_file_count, 0, "tree hardlink count");
|
|
1105
|
+
return value;
|
|
1106
|
+
}
|
|
1107
|
+
function readRuntimeAuthorization(file = BETA13_WINDOWS_X64_AUTHORIZATION) {
|
|
1108
|
+
return validateRuntimeAuthorization(JSON.parse(readFileSync2(file, "utf8")));
|
|
1109
|
+
}
|
|
1110
|
+
function validateStagedRuntimeAuthorization(options) {
|
|
1111
|
+
const authorization = validateRuntimeAuthorization(options?.authorization || readRuntimeAuthorization());
|
|
1112
|
+
return compareStagedRuntime({ ...options, authorization });
|
|
1113
|
+
}
|
|
1114
|
+
if (process.argv[1] && resolve2(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1115
|
+
readRuntimeAuthorization(process.argv[2] ? resolve2(process.argv[2]) : void 0);
|
|
1116
|
+
process.stdout.write("AlgoHQ runtime authorization valid\n");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/runner/bundled-runtime-store.mjs
|
|
1120
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
1121
|
+
import {
|
|
1122
|
+
closeSync,
|
|
1123
|
+
existsSync as existsSync3,
|
|
1124
|
+
fsyncSync,
|
|
1125
|
+
lstatSync as lstatSync2,
|
|
1126
|
+
mkdirSync,
|
|
1127
|
+
openSync,
|
|
1128
|
+
readFileSync as readFileSync3,
|
|
1129
|
+
readdirSync as readdirSync2,
|
|
1130
|
+
realpathSync,
|
|
1131
|
+
renameSync,
|
|
1132
|
+
rmSync,
|
|
1133
|
+
writeFileSync
|
|
1134
|
+
} from "node:fs";
|
|
1135
|
+
import { homedir } from "node:os";
|
|
1136
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve3 } from "node:path";
|
|
1137
|
+
var SLOT_ID_RE = /^vo-mcp-[0-9A-Za-z._-]{1,96}$/u;
|
|
1138
|
+
var ACTION_ID_RE = /^[0-9A-Za-z._-]{1,128}$/u;
|
|
1139
|
+
var INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
1140
|
+
var VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
1141
|
+
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;
|
|
1142
|
+
var ENTRY_REL = join2("node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp");
|
|
1143
|
+
var SUPERVISOR_REL = join2("node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js");
|
|
1144
|
+
var PACKAGE_REL = join2("node_modules", "@algosuite", "vo-mcp", "package.json");
|
|
1145
|
+
var CREDENTIAL_HELPER_REL = join2("node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js");
|
|
1146
|
+
var MANIFEST_FILE = "runtime-manifest.json";
|
|
1147
|
+
function within(parent, candidate) {
|
|
1148
|
+
const rel = relative2(resolve3(parent), resolve3(candidate));
|
|
1149
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
1150
|
+
}
|
|
1151
|
+
var APP_IDENTIFIER = "ai.algosuite.vo-runner";
|
|
1152
|
+
var RUNNER_RUNTIME_DIR = "runner-runtime";
|
|
1153
|
+
function defaultRuntimeRoot({ platform = process.platform, env = process.env, home = homedir() } = {}) {
|
|
1154
|
+
if (platform === "win32") {
|
|
1155
|
+
const appData = String(env.APPDATA || "").trim();
|
|
1156
|
+
return appData && isAbsolute2(appData) ? join2(appData, APP_IDENTIFIER, RUNNER_RUNTIME_DIR) : null;
|
|
1157
|
+
}
|
|
1158
|
+
if (!home) return null;
|
|
1159
|
+
if (platform === "darwin") {
|
|
1160
|
+
return join2(home, "Library", "Application Support", APP_IDENTIFIER, RUNNER_RUNTIME_DIR);
|
|
1161
|
+
}
|
|
1162
|
+
const xdg = String(env.XDG_CONFIG_HOME || "").trim();
|
|
1163
|
+
const base = xdg && isAbsolute2(xdg) ? xdg : join2(home, ".config");
|
|
1164
|
+
return join2(base, APP_IDENTIFIER, RUNNER_RUNTIME_DIR);
|
|
1165
|
+
}
|
|
1166
|
+
function runtimeRootFromEnv(env = process.env, { platform, home } = {}) {
|
|
1167
|
+
const value = String(env.VO_RUNNER_RUNTIME_ROOT || "").trim();
|
|
1168
|
+
if (value) return isAbsolute2(value) ? resolve3(value) : null;
|
|
1169
|
+
const derived = defaultRuntimeRoot({ platform, env, home });
|
|
1170
|
+
return derived ? resolve3(derived) : null;
|
|
1171
|
+
}
|
|
1172
|
+
function hashFileSha512(file) {
|
|
1173
|
+
return `sha512-${createHash3("sha512").update(readFileSync3(file)).digest("base64")}`;
|
|
1174
|
+
}
|
|
1175
|
+
function hashRuntimeTree(root) {
|
|
1176
|
+
const hasher = createHash3("sha512");
|
|
1177
|
+
const files = [];
|
|
1178
|
+
const visit = (directory, prefix = "") => {
|
|
1179
|
+
const rootStat = lstatSync2(directory);
|
|
1180
|
+
if (rootStat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
1181
|
+
if (!rootStat.isDirectory()) throw new Error("runtime tree root is not a directory");
|
|
1182
|
+
for (const name of readdirSync2(directory).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) {
|
|
1183
|
+
const absolute = join2(directory, name);
|
|
1184
|
+
const relativePath = prefix ? `${prefix}/${name}` : name;
|
|
1185
|
+
const stat = lstatSync2(absolute);
|
|
1186
|
+
if (stat.isSymbolicLink()) throw new Error("runtime tree contains a link/reparse point");
|
|
1187
|
+
if (stat.isDirectory()) visit(absolute, relativePath);
|
|
1188
|
+
else if (stat.isFile() && relativePath !== MANIFEST_FILE) files.push({ absolute, relativePath, size: stat.size });
|
|
1189
|
+
else if (!stat.isFile()) throw new Error("runtime tree contains a non-regular file");
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
visit(root);
|
|
1193
|
+
files.sort((a, b) => Buffer.compare(Buffer.from(a.relativePath), Buffer.from(b.relativePath)));
|
|
1194
|
+
for (const file of files) {
|
|
1195
|
+
const pathBytes = Buffer.from(file.relativePath, "utf8");
|
|
1196
|
+
hasher.update(`${pathBytes.length}:`);
|
|
1197
|
+
hasher.update(pathBytes);
|
|
1198
|
+
hasher.update(`:${file.size}:`);
|
|
1199
|
+
hasher.update(readFileSync3(file.absolute));
|
|
1200
|
+
hasher.update("\n");
|
|
1201
|
+
}
|
|
1202
|
+
return `sha512-${hasher.digest("base64")}`;
|
|
1203
|
+
}
|
|
1204
|
+
function atomicWriteJson(file, value) {
|
|
1205
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
1206
|
+
const temp = join2(dirname2(file), `.${randomUUID()}.tmp`);
|
|
1207
|
+
const fd = openSync(temp, "wx", 384);
|
|
1208
|
+
try {
|
|
1209
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}
|
|
1210
|
+
`, "utf8");
|
|
1211
|
+
fsyncSync(fd);
|
|
1212
|
+
} finally {
|
|
1213
|
+
closeSync(fd);
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
renameSync(temp, file);
|
|
1217
|
+
if (process.platform !== "win32") {
|
|
1218
|
+
try {
|
|
1219
|
+
const parentFd = openSync(dirname2(file), "r");
|
|
1220
|
+
try {
|
|
1221
|
+
fsyncSync(parentFd);
|
|
1222
|
+
} finally {
|
|
1223
|
+
closeSync(parentFd);
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
} finally {
|
|
1229
|
+
rmSync(temp, { force: true });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function readActivation(runtimeRoot) {
|
|
1233
|
+
const file = join2(runtimeRoot, "current.json");
|
|
1234
|
+
if (!existsSync3(file)) return null;
|
|
1235
|
+
try {
|
|
1236
|
+
const value = JSON.parse(readFileSync3(file, "utf8"));
|
|
1237
|
+
return value?.schema_version === 1 ? value : null;
|
|
1238
|
+
} catch {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function slotPaths(runtimeRoot, slotId) {
|
|
1243
|
+
if (!SLOT_ID_RE.test(slotId)) throw new Error("invalid runtime slot id");
|
|
1244
|
+
const slotRoot = join2(runtimeRoot, "slots", slotId);
|
|
1245
|
+
return {
|
|
1246
|
+
slotRoot,
|
|
1247
|
+
entry: join2(slotRoot, ENTRY_REL),
|
|
1248
|
+
supervisor: join2(slotRoot, SUPERVISOR_REL),
|
|
1249
|
+
packageJson: join2(slotRoot, PACKAGE_REL),
|
|
1250
|
+
credentialHelper: join2(slotRoot, CREDENTIAL_HELPER_REL),
|
|
1251
|
+
manifest: join2(slotRoot, MANIFEST_FILE)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function validActive(active) {
|
|
1255
|
+
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);
|
|
1256
|
+
}
|
|
1257
|
+
function validateSlot(runtimeRoot, active) {
|
|
1258
|
+
if (!validActive(active)) return { ok: false, detail: "invalid activation metadata" };
|
|
1259
|
+
const paths = slotPaths(runtimeRoot, active.slot_id);
|
|
1260
|
+
try {
|
|
1261
|
+
const manifest = JSON.parse(readFileSync3(paths.manifest, "utf8"));
|
|
1262
|
+
const pkg = JSON.parse(readFileSync3(paths.packageJson, "utf8"));
|
|
1263
|
+
const expected = {
|
|
1264
|
+
slot_id: active.slot_id,
|
|
1265
|
+
version: active.version,
|
|
1266
|
+
integrity: active.integrity,
|
|
1267
|
+
entry_sha512: active.entry_sha512,
|
|
1268
|
+
supervisor_sha512: active.supervisor_sha512,
|
|
1269
|
+
tree_sha512: active.tree_sha512
|
|
1270
|
+
};
|
|
1271
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1272
|
+
if (manifest?.[key] !== value) return { ok: false, detail: `manifest ${key} mismatch` };
|
|
1273
|
+
}
|
|
1274
|
+
if (manifest?.schema_version !== 1 || pkg?.name !== "@algosuite/vo-mcp" || pkg?.version !== active.version) {
|
|
1275
|
+
return { ok: false, detail: "package identity mismatch" };
|
|
1276
|
+
}
|
|
1277
|
+
if (lstatSync2(paths.entry).isSymbolicLink() || lstatSync2(paths.supervisor).isSymbolicLink() || lstatSync2(paths.credentialHelper).isSymbolicLink()) {
|
|
1278
|
+
return { ok: false, detail: "runtime entry cannot be a link" };
|
|
1279
|
+
}
|
|
1280
|
+
if (!within(paths.slotRoot, realpathSync(paths.entry)) || !within(paths.slotRoot, realpathSync(paths.supervisor)) || !within(paths.slotRoot, realpathSync(paths.credentialHelper))) {
|
|
1281
|
+
return { ok: false, detail: "runtime entry escaped its slot" };
|
|
1282
|
+
}
|
|
1283
|
+
if (hashFileSha512(paths.entry) !== active.entry_sha512) return { ok: false, detail: "entry hash mismatch" };
|
|
1284
|
+
if (hashFileSha512(paths.supervisor) !== active.supervisor_sha512) return { ok: false, detail: "supervisor hash mismatch" };
|
|
1285
|
+
if (hashRuntimeTree(paths.slotRoot) !== active.tree_sha512) return { ok: false, detail: "runtime tree hash mismatch" };
|
|
1286
|
+
return { ok: true, paths, manifest };
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function journalActivation(runtimeRoot, actionId, state, detail = "") {
|
|
1292
|
+
if (!ACTION_ID_RE.test(actionId)) throw new Error("invalid runner action id");
|
|
1293
|
+
atomicWriteJson(join2(runtimeRoot, "transactions", `${actionId}.json`), {
|
|
1294
|
+
schema_version: 1,
|
|
1295
|
+
action_id: actionId,
|
|
1296
|
+
state,
|
|
1297
|
+
detail: String(detail).slice(0, 400),
|
|
1298
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function activateSlot(runtimeRoot, active, action) {
|
|
1302
|
+
if (!validActive(active)) throw new Error("cannot activate invalid runtime metadata");
|
|
1303
|
+
if (!ACTION_ID_RE.test(action.actionId)) throw new Error("invalid runner action id");
|
|
1304
|
+
if (!UUID_RE.test(String(action.supervisorInstanceId || ""))) throw new Error("invalid claiming supervisor instance id");
|
|
1305
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
1306
|
+
if (!validated.ok) throw new Error(`cannot activate invalid runtime slot: ${validated.detail}`);
|
|
1307
|
+
const current = readActivation(runtimeRoot);
|
|
1308
|
+
if (current?.pending) throw new Error("another runtime activation is still pending");
|
|
1309
|
+
const pointer = {
|
|
1310
|
+
schema_version: 1,
|
|
1311
|
+
generation: randomUUID(),
|
|
1312
|
+
active,
|
|
1313
|
+
previous: validActive(current?.active) ? current.active : null,
|
|
1314
|
+
pending: {
|
|
1315
|
+
action_id: action.actionId,
|
|
1316
|
+
runner_id: String(action.runnerId || ""),
|
|
1317
|
+
operator_id: String(action.operatorId || ""),
|
|
1318
|
+
supervisor_instance_id: action.supervisorInstanceId,
|
|
1319
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1320
|
+
ack_attempts: 0
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
journalActivation(runtimeRoot, action.actionId, "prepared", `${active.version} ${active.integrity}`);
|
|
1324
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), pointer);
|
|
1325
|
+
return pointer;
|
|
1326
|
+
}
|
|
1327
|
+
function activationSupervisorInstanceId(runtimeRoot, fallback) {
|
|
1328
|
+
const current = runtimeRoot ? readActivation(runtimeRoot) : null;
|
|
1329
|
+
const pending = String(current?.pending?.supervisor_instance_id || "");
|
|
1330
|
+
if (UUID_RE.test(pending)) return pending;
|
|
1331
|
+
return UUID_RE.test(String(fallback || "")) ? fallback : null;
|
|
1332
|
+
}
|
|
1333
|
+
function recordActivationRetry(runtimeRoot, pointer, detail) {
|
|
1334
|
+
const current = readActivation(runtimeRoot);
|
|
1335
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
1336
|
+
throw new Error("runtime activation generation changed before retry");
|
|
1337
|
+
}
|
|
1338
|
+
const attempts = Math.max(0, Number(current.pending.ack_attempts || 0)) + 1;
|
|
1339
|
+
const updated = {
|
|
1340
|
+
...current,
|
|
1341
|
+
pending: { ...current.pending, ack_attempts: attempts, last_error: String(detail).slice(0, 240) }
|
|
1342
|
+
};
|
|
1343
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), updated);
|
|
1344
|
+
journalActivation(runtimeRoot, current.pending.action_id, "ack-retry", `attempt ${attempts}: ${detail}`);
|
|
1345
|
+
return updated;
|
|
1346
|
+
}
|
|
1347
|
+
function attestCurrentSupervisor({ runtimeRoot, selfPath: selfPath2, version }) {
|
|
1348
|
+
const pointer = readActivation(runtimeRoot);
|
|
1349
|
+
if (!pointer?.pending || !validActive(pointer.active)) return { ok: false, detail: "no pending activation" };
|
|
1350
|
+
const validated = validateSlot(runtimeRoot, pointer.active);
|
|
1351
|
+
if (!validated.ok) return validated;
|
|
1352
|
+
try {
|
|
1353
|
+
if (realpathSync(selfPath2) !== realpathSync(validated.paths.supervisor)) {
|
|
1354
|
+
return { ok: false, detail: "running supervisor is not the activated supervisor" };
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
return { ok: false, detail: "could not resolve running supervisor path" };
|
|
1358
|
+
}
|
|
1359
|
+
if (version !== pointer.active.version) return { ok: false, detail: "running supervisor version mismatch" };
|
|
1360
|
+
return { ok: true, pointer, active: pointer.active, paths: validated.paths };
|
|
1361
|
+
}
|
|
1362
|
+
function finalizeActivation(runtimeRoot, pointer) {
|
|
1363
|
+
const current = readActivation(runtimeRoot);
|
|
1364
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
1365
|
+
throw new Error("runtime activation generation changed before finalization");
|
|
1366
|
+
}
|
|
1367
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "attesting", `${pointer.active.version} verified`);
|
|
1368
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), {
|
|
1369
|
+
schema_version: 1,
|
|
1370
|
+
generation: pointer.generation,
|
|
1371
|
+
active: pointer.active,
|
|
1372
|
+
previous: pointer.previous || null,
|
|
1373
|
+
pending: null
|
|
1374
|
+
});
|
|
1375
|
+
try {
|
|
1376
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "attested", `${pointer.active.version} active`);
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function rollbackActivation(runtimeRoot, pointer, detail) {
|
|
1381
|
+
const current = readActivation(runtimeRoot);
|
|
1382
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id) {
|
|
1383
|
+
throw new Error("runtime activation generation changed before rollback");
|
|
1384
|
+
}
|
|
1385
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "rolling-back", detail);
|
|
1386
|
+
const rolledBack = {
|
|
1387
|
+
schema_version: 1,
|
|
1388
|
+
generation: randomUUID(),
|
|
1389
|
+
active: validActive(pointer?.previous) ? pointer.previous : null,
|
|
1390
|
+
previous: null,
|
|
1391
|
+
pending: {
|
|
1392
|
+
...pointer.pending,
|
|
1393
|
+
terminal_status: "failed",
|
|
1394
|
+
terminal_detail: String(detail).slice(0, 400),
|
|
1395
|
+
rolled_back_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), rolledBack);
|
|
1399
|
+
try {
|
|
1400
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "rolled-back", detail);
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
return rolledBack;
|
|
1404
|
+
}
|
|
1405
|
+
function acknowledgeActivationFailure(runtimeRoot, pointer) {
|
|
1406
|
+
const current = readActivation(runtimeRoot);
|
|
1407
|
+
if (current?.generation !== pointer?.generation || current?.pending?.action_id !== pointer?.pending?.action_id || current?.pending?.terminal_status !== "failed") {
|
|
1408
|
+
throw new Error("runtime rollback acknowledgement obligation changed");
|
|
1409
|
+
}
|
|
1410
|
+
atomicWriteJson(join2(runtimeRoot, "current.json"), { ...current, pending: null });
|
|
1411
|
+
try {
|
|
1412
|
+
journalActivation(runtimeRoot, pointer.pending.action_id, "failure-acknowledged", pointer.pending.terminal_detail);
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/runner/bundled-runtime-updater.mjs
|
|
1418
|
+
var PACKAGE_NAME = "@algosuite/vo-mcp";
|
|
1419
|
+
var PACKAGE_SPEC_RE2 = /^@algosuite\/vo-mcp@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
1420
|
+
var INTEGRITY_RE2 = /^sha512-[A-Za-z0-9+/]{86}==$/u;
|
|
1421
|
+
var MAX_TARBALL_BYTES = 100 * 1024 * 1024;
|
|
1422
|
+
var PUBLIC_REGISTRY = "https://registry.npmjs.org/";
|
|
1423
|
+
function buildMinimalMaintenanceEnv(env = process.env, runtimeRoot = "") {
|
|
1424
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1425
|
+
"PATH",
|
|
1426
|
+
"Path",
|
|
1427
|
+
"path",
|
|
1428
|
+
"PATHEXT",
|
|
1429
|
+
"SystemRoot",
|
|
1430
|
+
"SYSTEMROOT",
|
|
1431
|
+
"WINDIR",
|
|
1432
|
+
"COMSPEC",
|
|
1433
|
+
"TEMP",
|
|
1434
|
+
"TMP",
|
|
1435
|
+
"TMPDIR",
|
|
1436
|
+
"HOME",
|
|
1437
|
+
"USERPROFILE",
|
|
1438
|
+
"APPDATA",
|
|
1439
|
+
"LOCALAPPDATA",
|
|
1440
|
+
"ProgramFiles",
|
|
1441
|
+
"ProgramFiles(x86)",
|
|
1442
|
+
"ProgramW6432",
|
|
1443
|
+
"LANG",
|
|
1444
|
+
"LC_ALL"
|
|
1445
|
+
]);
|
|
1446
|
+
const clean = {};
|
|
1447
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1448
|
+
if (allowed.has(key) && typeof value === "string") clean[key] = value;
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
...clean,
|
|
1452
|
+
npm_config_ignore_scripts: "true",
|
|
1453
|
+
npm_config_bin_links: "false",
|
|
1454
|
+
npm_config_audit: "false",
|
|
1455
|
+
npm_config_fund: "false",
|
|
1456
|
+
npm_config_update_notifier: "false",
|
|
1457
|
+
npm_config_registry: PUBLIC_REGISTRY,
|
|
1458
|
+
...runtimeRoot ? {
|
|
1459
|
+
npm_config_userconfig: join3(runtimeRoot, "maintenance", "user.npmrc"),
|
|
1460
|
+
npm_config_globalconfig: join3(runtimeRoot, "maintenance", "global.npmrc"),
|
|
1461
|
+
npm_config_cache: join3(runtimeRoot, "maintenance", "npm-cache")
|
|
1462
|
+
} : {}
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function defaultRun(command, args, options) {
|
|
1466
|
+
return spawnSync2(command, args, {
|
|
1467
|
+
cwd: options.cwd,
|
|
1468
|
+
encoding: "utf8",
|
|
1469
|
+
env: options.env,
|
|
1470
|
+
shell: false,
|
|
1471
|
+
windowsHide: true,
|
|
1472
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1473
|
+
timeout: options.timeout ?? 12e4
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
function commandRunner({ platform, execPath, env, fileExists, run }) {
|
|
1477
|
+
const npmCli = platform === "win32" ? resolveNpmCli({ env, execPath, fileExists }) : null;
|
|
1478
|
+
if (platform === "win32" && !npmCli) throw new Error("trusted npm CLI not found");
|
|
1479
|
+
const npmCommand = platform === "win32" ? execPath : "npm";
|
|
1480
|
+
const prefix = npmCli ? [npmCli] : [];
|
|
1481
|
+
return {
|
|
1482
|
+
npm(args, options) {
|
|
1483
|
+
return run(npmCommand, [...prefix, ...args], options);
|
|
1484
|
+
},
|
|
1485
|
+
node(args, options) {
|
|
1486
|
+
return run(execPath, args, options);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function parseJsonOutput(result, operation) {
|
|
1491
|
+
if (result?.status !== 0) {
|
|
1492
|
+
throw new Error(`${operation} failed: ${String(result?.stderr || result?.error?.message || `exit ${result?.status ?? 1}`)}`);
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
return JSON.parse(String(result.stdout || ""));
|
|
1496
|
+
} catch {
|
|
1497
|
+
throw new Error(`${operation} returned invalid JSON`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function tarballIntegrity(file) {
|
|
1501
|
+
return `sha512-${createHash4("sha512").update(readFileSync4(file)).digest("base64")}`;
|
|
1502
|
+
}
|
|
1503
|
+
function assertNoLinks(root) {
|
|
1504
|
+
const pending = [root];
|
|
1505
|
+
while (pending.length) {
|
|
1506
|
+
const current = pending.pop();
|
|
1507
|
+
const stat = lstatSync3(current);
|
|
1508
|
+
if (stat.isSymbolicLink()) throw new Error("installed runtime contains a link/reparse point");
|
|
1509
|
+
if (!stat.isDirectory()) continue;
|
|
1510
|
+
for (const entry of readdirSync3(current)) pending.push(join3(current, entry));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function validateDependencyLock(payloadRoot, expected) {
|
|
1514
|
+
const lock = JSON.parse(readFileSync4(join3(payloadRoot, "package-lock.json"), "utf8"));
|
|
1515
|
+
if (Number(lock.lockfileVersion) < 3 || !lock.packages || typeof lock.packages !== "object") {
|
|
1516
|
+
throw new Error("runtime dependency lock is missing or unsupported");
|
|
1517
|
+
}
|
|
1518
|
+
let foundPackage = false;
|
|
1519
|
+
for (const [key, item] of Object.entries(lock.packages)) {
|
|
1520
|
+
if (!key) continue;
|
|
1521
|
+
if (item?.link === true) throw new Error(`runtime dependency lock contains link: ${key}`);
|
|
1522
|
+
const isRunner = key.replaceAll("\\", "/").endsWith("node_modules/@algosuite/vo-mcp");
|
|
1523
|
+
if (isRunner) {
|
|
1524
|
+
foundPackage = item.version === expected.version && item.integrity === expected.integrity;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (!INTEGRITY_RE2.test(String(item?.integrity || ""))) throw new Error(`dependency lacks sha512 integrity: ${key}`);
|
|
1528
|
+
if (!String(item?.resolved || "").startsWith("https://registry.npmjs.org/")) {
|
|
1529
|
+
throw new Error(`dependency is not registry-pinned: ${key}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (!foundPackage) throw new Error("installed runner package does not match registry integrity");
|
|
1533
|
+
}
|
|
1534
|
+
function writeAuthorizedInstallSeed(payloadRoot, tarball, runtimeAuthorization) {
|
|
1535
|
+
const authorization = validateRuntimeAuthorization(runtimeAuthorization);
|
|
1536
|
+
const stagingRoot = resolve4(payloadRoot, "..", "..");
|
|
1537
|
+
const tarballFromStaging = relative3(stagingRoot, resolve4(tarball));
|
|
1538
|
+
if (!tarballFromStaging || tarballFromStaging === ".." || tarballFromStaging.startsWith(`..${sep2}`) || isAbsolute3(tarballFromStaging)) {
|
|
1539
|
+
throw new Error("authorized runtime tarball escaped staging root");
|
|
1540
|
+
}
|
|
1541
|
+
const relativeTarball = relative3(payloadRoot, resolve4(tarball)).replaceAll("\\", "/");
|
|
1542
|
+
if (!relativeTarball || isAbsolute3(relativeTarball) || relativeTarball.includes("\n") || relativeTarball.includes("\r")) {
|
|
1543
|
+
throw new Error("authorized runtime tarball path invalid");
|
|
1544
|
+
}
|
|
1545
|
+
const fileSpec = `file:${relativeTarball}`;
|
|
1546
|
+
const packageRecord = {
|
|
1547
|
+
name: "algohq-runner-runtime",
|
|
1548
|
+
version: "0.0.0",
|
|
1549
|
+
private: true,
|
|
1550
|
+
dependencies: { [PACKAGE_NAME]: fileSpec }
|
|
1551
|
+
};
|
|
1552
|
+
const packages = structuredClone(authorization.dependency_lock.packages);
|
|
1553
|
+
packages[`node_modules/${PACKAGE_NAME}`].resolved = fileSpec;
|
|
1554
|
+
const lock = {
|
|
1555
|
+
name: packageRecord.name,
|
|
1556
|
+
version: packageRecord.version,
|
|
1557
|
+
lockfileVersion: authorization.dependency_lock.source_lockfile_version,
|
|
1558
|
+
requires: true,
|
|
1559
|
+
packages: { "": packageRecord, ...packages }
|
|
1560
|
+
};
|
|
1561
|
+
writeFileSync2(join3(payloadRoot, "package.json"), `${JSON.stringify(packageRecord)}
|
|
1562
|
+
`, { mode: 384 });
|
|
1563
|
+
writeFileSync2(join3(payloadRoot, "package-lock.json"), `${JSON.stringify(lock)}
|
|
1564
|
+
`, { mode: 384 });
|
|
1565
|
+
return { fileSpec, lock };
|
|
1566
|
+
}
|
|
1567
|
+
function buildActive(slotId, metadata, paths) {
|
|
1568
|
+
return {
|
|
1569
|
+
slot_id: slotId,
|
|
1570
|
+
version: metadata.version,
|
|
1571
|
+
integrity: metadata.integrity,
|
|
1572
|
+
entry_sha512: hashFileSha512(paths.entry),
|
|
1573
|
+
supervisor_sha512: hashFileSha512(paths.supervisor),
|
|
1574
|
+
tree_sha512: hashRuntimeTree(paths.slotRoot ?? paths.payloadRoot)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function installSlot({
|
|
1578
|
+
runtimeRoot,
|
|
1579
|
+
metadata,
|
|
1580
|
+
tarball,
|
|
1581
|
+
runner,
|
|
1582
|
+
npmEnv,
|
|
1583
|
+
runOptions,
|
|
1584
|
+
force,
|
|
1585
|
+
runtimeAuthorization
|
|
1586
|
+
}) {
|
|
1587
|
+
const digest = createHash4("sha256").update(metadata.integrity).digest("hex").slice(0, 16);
|
|
1588
|
+
const suffix = force ? `${digest}-${randomUUID2().slice(0, 8)}` : digest;
|
|
1589
|
+
const slotId = `vo-mcp-${metadata.version}-${suffix}`;
|
|
1590
|
+
const finalPaths = slotPaths(runtimeRoot, slotId);
|
|
1591
|
+
if (!force && existsSync4(finalPaths.slotRoot)) {
|
|
1592
|
+
const manifest = JSON.parse(readFileSync4(finalPaths.manifest, "utf8"));
|
|
1593
|
+
const active = buildActive(slotId, metadata, finalPaths);
|
|
1594
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
1595
|
+
if (validated.ok && manifest.integrity === metadata.integrity) {
|
|
1596
|
+
if (runtimeAuthorization) {
|
|
1597
|
+
validateStagedRuntimeAuthorization({
|
|
1598
|
+
payloadRoot: finalPaths.slotRoot,
|
|
1599
|
+
authorization: runtimeAuthorization
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
return { active, created: false };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const staging = join3(runtimeRoot, "staging", randomUUID2());
|
|
1606
|
+
const payload = join3(staging, "payload");
|
|
1607
|
+
let installedSlot = false;
|
|
1608
|
+
try {
|
|
1609
|
+
mkdirSync2(payload, { recursive: true });
|
|
1610
|
+
let installArgs;
|
|
1611
|
+
if (runtimeAuthorization) {
|
|
1612
|
+
writeAuthorizedInstallSeed(payload, tarball, runtimeAuthorization);
|
|
1613
|
+
installArgs = [
|
|
1614
|
+
"ci",
|
|
1615
|
+
"--ignore-scripts",
|
|
1616
|
+
"--no-bin-links",
|
|
1617
|
+
"--no-audit",
|
|
1618
|
+
"--no-fund",
|
|
1619
|
+
`--registry=${PUBLIC_REGISTRY}`
|
|
1620
|
+
];
|
|
1621
|
+
} else {
|
|
1622
|
+
writeFileSync2(join3(payload, "package.json"), `${JSON.stringify({
|
|
1623
|
+
name: "algohq-runner-runtime",
|
|
1624
|
+
version: "0.0.0",
|
|
1625
|
+
private: true
|
|
1626
|
+
})}
|
|
1627
|
+
`);
|
|
1628
|
+
installArgs = [
|
|
1629
|
+
"install",
|
|
1630
|
+
"--ignore-scripts",
|
|
1631
|
+
"--no-bin-links",
|
|
1632
|
+
"--no-audit",
|
|
1633
|
+
"--no-fund",
|
|
1634
|
+
"--package-lock=true",
|
|
1635
|
+
"--save-exact",
|
|
1636
|
+
`--registry=${PUBLIC_REGISTRY}`,
|
|
1637
|
+
tarball
|
|
1638
|
+
];
|
|
1639
|
+
}
|
|
1640
|
+
const install = runner.npm(
|
|
1641
|
+
installArgs,
|
|
1642
|
+
{ ...runOptions, cwd: payload, env: npmEnv, timeout: 18e4 }
|
|
1643
|
+
);
|
|
1644
|
+
if (install.status !== 0) throw new Error(`npm install failed: ${install.stderr || install.error?.message || install.status}`);
|
|
1645
|
+
assertNoLinks(payload);
|
|
1646
|
+
validateDependencyLock(payload, metadata);
|
|
1647
|
+
if (runtimeAuthorization) {
|
|
1648
|
+
validateStagedRuntimeAuthorization({ payloadRoot: payload, authorization: runtimeAuthorization });
|
|
1649
|
+
}
|
|
1650
|
+
const stagedPaths = {
|
|
1651
|
+
entry: join3(payload, "node_modules", "@algosuite", "vo-mcp", "bin", "vo-mcp"),
|
|
1652
|
+
supervisor: join3(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "runner-supervisor.js"),
|
|
1653
|
+
packageJson: join3(payload, "node_modules", "@algosuite", "vo-mcp", "package.json"),
|
|
1654
|
+
credentialHelper: join3(payload, "node_modules", "@algosuite", "vo-mcp", "dist", "supervisor-credential-helper.js"),
|
|
1655
|
+
slotRoot: payload
|
|
1656
|
+
};
|
|
1657
|
+
const pkg = JSON.parse(readFileSync4(stagedPaths.packageJson, "utf8"));
|
|
1658
|
+
if (pkg.name !== PACKAGE_NAME || pkg.version !== metadata.version) throw new Error("installed package identity mismatch");
|
|
1659
|
+
if (!lstatSync3(stagedPaths.credentialHelper).isFile()) throw new Error("installed credential helper is missing");
|
|
1660
|
+
const smoke = runner.node([stagedPaths.entry, "runner", "--version"], { ...runOptions, cwd: payload, env: npmEnv, timeout: 3e4 });
|
|
1661
|
+
if (smoke.status !== 0 || String(smoke.stdout || "").trim() !== `vo-mcp runner ${metadata.version}`) {
|
|
1662
|
+
throw new Error("bundled runtime smoke check failed");
|
|
1663
|
+
}
|
|
1664
|
+
const active = buildActive(slotId, metadata, stagedPaths);
|
|
1665
|
+
atomicWriteJson(join3(payload, "runtime-manifest.json"), { schema_version: 1, ...active });
|
|
1666
|
+
mkdirSync2(join3(runtimeRoot, "slots"), { recursive: true });
|
|
1667
|
+
if (existsSync4(finalPaths.slotRoot)) throw new Error("immutable runtime slot already exists");
|
|
1668
|
+
renameSync2(payload, finalPaths.slotRoot);
|
|
1669
|
+
installedSlot = true;
|
|
1670
|
+
const validated = validateSlot(runtimeRoot, active);
|
|
1671
|
+
if (!validated.ok) throw new Error(`staged runtime validation failed: ${validated.detail}`);
|
|
1672
|
+
return { active, created: true };
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
if (installedSlot) rmSync2(finalPaths.slotRoot, { recursive: true, force: true });
|
|
1675
|
+
throw error;
|
|
1676
|
+
} finally {
|
|
1677
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function stageBundledRuntimeSlot(options) {
|
|
1681
|
+
const {
|
|
1682
|
+
runtimeRoot,
|
|
1683
|
+
packageSpec,
|
|
1684
|
+
expectedVersion,
|
|
1685
|
+
expectedIntegrity,
|
|
1686
|
+
platform = process.platform,
|
|
1687
|
+
execPath = process.execPath,
|
|
1688
|
+
env = process.env,
|
|
1689
|
+
fileExists = existsSync4,
|
|
1690
|
+
run = defaultRun,
|
|
1691
|
+
force = false,
|
|
1692
|
+
runtimeAuthorization = null
|
|
1693
|
+
} = options;
|
|
1694
|
+
if (!runtimeRoot || !isAbsolute3(runtimeRoot)) return { ok: false, status: 2, detail: "bundled runtime root unavailable" };
|
|
1695
|
+
if (!expectedVersion || !PACKAGE_SPEC_RE2.test(`${PACKAGE_NAME}@${expectedVersion}`)) {
|
|
1696
|
+
return { ok: false, status: 2, detail: "invalid expected runner version" };
|
|
1697
|
+
}
|
|
1698
|
+
if (!expectedIntegrity || !INTEGRITY_RE2.test(expectedIntegrity)) {
|
|
1699
|
+
return { ok: false, status: 2, detail: "invalid expected runner integrity" };
|
|
1700
|
+
}
|
|
1701
|
+
const exactSpec = `${PACKAGE_NAME}@${expectedVersion}`;
|
|
1702
|
+
if (packageSpec !== exactSpec) return { ok: false, status: 2, detail: "runner package spec does not match authorized version" };
|
|
1703
|
+
const resolvedRoot = resolve4(runtimeRoot);
|
|
1704
|
+
const npmEnv = buildMinimalMaintenanceEnv(env, resolvedRoot);
|
|
1705
|
+
const runOptions = { env: npmEnv, cwd: resolvedRoot };
|
|
1706
|
+
let tarDir = null;
|
|
1707
|
+
try {
|
|
1708
|
+
mkdirSync2(resolvedRoot, { recursive: true });
|
|
1709
|
+
mkdirSync2(join3(resolvedRoot, "maintenance"), { recursive: true });
|
|
1710
|
+
writeFileSync2(npmEnv.npm_config_userconfig, "", { mode: 384 });
|
|
1711
|
+
writeFileSync2(npmEnv.npm_config_globalconfig, "", { mode: 384 });
|
|
1712
|
+
const runner = commandRunner({ platform, execPath, env: npmEnv, fileExists, run });
|
|
1713
|
+
const metadata = { version: expectedVersion, integrity: expectedIntegrity };
|
|
1714
|
+
tarDir = join3(resolvedRoot, "staging", randomUUID2());
|
|
1715
|
+
mkdirSync2(tarDir, { recursive: true });
|
|
1716
|
+
const packed = parseJsonOutput(runner.npm([
|
|
1717
|
+
"pack",
|
|
1718
|
+
exactSpec,
|
|
1719
|
+
"--ignore-scripts",
|
|
1720
|
+
"--json",
|
|
1721
|
+
"--pack-destination",
|
|
1722
|
+
tarDir,
|
|
1723
|
+
`--registry=${PUBLIC_REGISTRY}`
|
|
1724
|
+
], runOptions), "npm pack");
|
|
1725
|
+
const record = Array.isArray(packed) ? packed[0] : packed;
|
|
1726
|
+
const tarball = join3(tarDir, basename(String(record?.filename || "")));
|
|
1727
|
+
if (!existsSync4(tarball) || !basename(tarball).endsWith(".tgz")) throw new Error("npm pack returned no tarball");
|
|
1728
|
+
if (lstatSync3(tarball).size > MAX_TARBALL_BYTES) throw new Error("runner package tarball exceeds size limit");
|
|
1729
|
+
if (record.integrity !== metadata.integrity || tarballIntegrity(tarball) !== metadata.integrity) {
|
|
1730
|
+
throw new Error("runner package sha512 integrity mismatch");
|
|
1731
|
+
}
|
|
1732
|
+
const installed = installSlot({
|
|
1733
|
+
runtimeRoot: resolvedRoot,
|
|
1734
|
+
metadata,
|
|
1735
|
+
tarball,
|
|
1736
|
+
runner,
|
|
1737
|
+
npmEnv,
|
|
1738
|
+
runOptions,
|
|
1739
|
+
force,
|
|
1740
|
+
runtimeAuthorization
|
|
1741
|
+
});
|
|
1742
|
+
return { ok: true, status: 0, ...installed };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
return { ok: false, status: 1, detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env) };
|
|
1745
|
+
} finally {
|
|
1746
|
+
if (tarDir) rmSync2(tarDir, { recursive: true, force: true });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function stageAndActivateBundledUpdate(options) {
|
|
1750
|
+
const staged = stageBundledRuntimeSlot(options);
|
|
1751
|
+
if (!staged.ok) return staged;
|
|
1752
|
+
try {
|
|
1753
|
+
activateSlot(resolve4(options.runtimeRoot), staged.active, options.action);
|
|
1754
|
+
return { ...staged, handoff: true };
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
return {
|
|
1757
|
+
ok: false,
|
|
1758
|
+
status: 1,
|
|
1759
|
+
detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), options.env ?? process.env)
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// ../../scripts/virtual-office/code-runner/legacy-orphan-sweep.mjs
|
|
1765
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1766
|
+
|
|
1767
|
+
// ../../scripts/virtual-office/code-runner/orphan-agent-reaper.mjs
|
|
1768
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1769
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1770
|
+
import os from "node:os";
|
|
1771
|
+
import path from "node:path";
|
|
1772
|
+
function windowsSystemRoot(env = process.env) {
|
|
1773
|
+
return env.SystemRoot || env.WINDIR || "C:\\Windows";
|
|
1774
|
+
}
|
|
1775
|
+
function windowsPowershellExe(env = process.env) {
|
|
1776
|
+
return path.join(windowsSystemRoot(env), "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1777
|
+
}
|
|
1778
|
+
function killProcessTree(pid, { platform = process.platform, spawn: spawn2 = spawnSync3, env = process.env } = {}) {
|
|
1779
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1780
|
+
if (platform === "win32") {
|
|
1781
|
+
const taskkill = path.join(windowsSystemRoot(env), "System32", "taskkill.exe");
|
|
1782
|
+
const r = spawn2(taskkill, ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore", timeout: 15e3 });
|
|
1783
|
+
return !r.error && r.status === 0;
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
process.kill(-pid, "SIGKILL");
|
|
1787
|
+
return true;
|
|
1788
|
+
} catch {
|
|
1789
|
+
try {
|
|
1790
|
+
process.kill(pid, "SIGKILL");
|
|
1791
|
+
return true;
|
|
1792
|
+
} catch {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// ../../scripts/virtual-office/code-runner/legacy-orphan-sweep.mjs
|
|
1799
|
+
var MAX_LEGACY_KILLS = 50;
|
|
1800
|
+
var SWEEP_RECENCY_BUFFER_MS = 5e3;
|
|
1801
|
+
var SIGNATURES = [
|
|
1802
|
+
{
|
|
1803
|
+
signature: "claude-headless",
|
|
1804
|
+
// claude-args.mjs always emits `-p --output-format stream-json --verbose`.
|
|
1805
|
+
test: (cl) => /(?:^|[\\/"\s])claude(?:\.exe|\.cmd|\.ps1)?(?:"|\s)/iu.test(cl) && /--output-format[\s"=]+stream-json/iu.test(cl)
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
signature: "codex-headless",
|
|
1809
|
+
// openai-compatible-runner always emits `exec --json`.
|
|
1810
|
+
test: (cl) => /(?:^|[\\/"\s])codex(?:\.exe|\.cmd|\.ps1)?(?:"|\s)/iu.test(cl) && /\bexec\b/u.test(cl) && /--json\b/u.test(cl)
|
|
1811
|
+
}
|
|
1812
|
+
];
|
|
1813
|
+
function matchAgentSignature(commandLine) {
|
|
1814
|
+
if (typeof commandLine !== "string" || !commandLine) return null;
|
|
1815
|
+
for (const { signature, test } of SIGNATURES) {
|
|
1816
|
+
if (test(commandLine)) return signature;
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
function selectLegacyOrphans({ processes, cutoffMs, protectedPids = /* @__PURE__ */ new Set() }) {
|
|
1821
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const proc of processes) {
|
|
1823
|
+
if (Number.isInteger(proc?.pid) && proc.pid > 0) byPid.set(proc.pid, proc);
|
|
1824
|
+
}
|
|
1825
|
+
const kills = [];
|
|
1826
|
+
for (const proc of byPid.values()) {
|
|
1827
|
+
if (protectedPids.has(proc.pid)) continue;
|
|
1828
|
+
if (!(Number.isFinite(proc.creationMs) && proc.creationMs < cutoffMs)) continue;
|
|
1829
|
+
const signature = matchAgentSignature(proc.commandLine);
|
|
1830
|
+
if (!signature) continue;
|
|
1831
|
+
const parent = Number.isInteger(proc.ppid) && proc.ppid > 0 ? byPid.get(proc.ppid) : void 0;
|
|
1832
|
+
const parentDead = !parent || Number.isFinite(parent.creationMs) && parent.creationMs > proc.creationMs;
|
|
1833
|
+
if (!parentDead) continue;
|
|
1834
|
+
kills.push({ pid: proc.pid, creationMs: proc.creationMs, signature, commandLine: proc.commandLine });
|
|
1835
|
+
}
|
|
1836
|
+
kills.sort((a, b) => a.creationMs - b.creationMs);
|
|
1837
|
+
return { kills: kills.slice(0, MAX_LEGACY_KILLS) };
|
|
1838
|
+
}
|
|
1839
|
+
function parsePosixSweepLine(line, nowMs) {
|
|
1840
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/u.exec(line ?? "");
|
|
1841
|
+
if (!match) return null;
|
|
1842
|
+
const pid = Number(match[1]);
|
|
1843
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
1844
|
+
return {
|
|
1845
|
+
pid,
|
|
1846
|
+
ppid: Number(match[2]),
|
|
1847
|
+
creationMs: nowMs - Number(match[3]) * 1e3,
|
|
1848
|
+
commandLine: match[4]
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function listProcessesForSweep({ platform = process.platform, spawn: spawn2 = spawnSync4, nowMs = Date.now(), env = process.env, warn = console.warn } = {}) {
|
|
1852
|
+
const rows = [];
|
|
1853
|
+
if (platform === "win32") {
|
|
1854
|
+
const ps = "Get-CimInstance Win32_Process | Where-Object { $_.CreationDate } | ForEach-Object { @{ p = $_.ProcessId; pp = $_.ParentProcessId; c = (([DateTimeOffset]$_.CreationDate.ToUniversalTime()).ToUnixTimeMilliseconds()); cl = [string]$_.CommandLine } | ConvertTo-Json -Compress }";
|
|
1855
|
+
const result2 = spawn2(windowsPowershellExe(env), ["-NoProfile", "-NonInteractive", "-Command", ps], {
|
|
1856
|
+
windowsHide: true,
|
|
1857
|
+
encoding: "utf8",
|
|
1858
|
+
timeout: 3e4,
|
|
1859
|
+
maxBuffer: 64 * 1024 * 1024
|
|
1860
|
+
});
|
|
1861
|
+
if (result2.error || result2.status !== 0) {
|
|
1862
|
+
const cause = result2.error ? result2.error.message : `powershell exit ${result2.status}`;
|
|
1863
|
+
warn(`[orphan-sweep] process enumeration failed (${cause}); sweeping nothing this cycle`);
|
|
1864
|
+
throw new Error(`process enumeration failed (${cause}); swept nothing`);
|
|
1865
|
+
}
|
|
1866
|
+
for (const line of String(result2.stdout ?? "").split(/\r?\n/u)) {
|
|
1867
|
+
if (!line.trim()) continue;
|
|
1868
|
+
try {
|
|
1869
|
+
const parsed = JSON.parse(line);
|
|
1870
|
+
const pid = Number(parsed?.p);
|
|
1871
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
1872
|
+
rows.push({
|
|
1873
|
+
pid,
|
|
1874
|
+
ppid: Number(parsed.pp),
|
|
1875
|
+
creationMs: Number(parsed.c),
|
|
1876
|
+
commandLine: typeof parsed.cl === "string" ? parsed.cl : ""
|
|
1877
|
+
});
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return rows;
|
|
1882
|
+
}
|
|
1883
|
+
const result = spawn2("ps", ["-eo", "pid=,ppid=,etimes=,args="], { encoding: "utf8", timeout: 3e4, maxBuffer: 64 * 1024 * 1024 });
|
|
1884
|
+
if (result.error || result.status !== 0) {
|
|
1885
|
+
const cause = result.error ? result.error.message : `ps exit ${result.status}`;
|
|
1886
|
+
warn(`[orphan-sweep] process enumeration failed (${cause}); sweeping nothing this cycle`);
|
|
1887
|
+
throw new Error(`process enumeration failed (${cause}); swept nothing`);
|
|
1888
|
+
}
|
|
1889
|
+
for (const line of String(result.stdout ?? "").split("\n")) {
|
|
1890
|
+
const row = parsePosixSweepLine(line, nowMs);
|
|
1891
|
+
if (row) rows.push(row);
|
|
1892
|
+
}
|
|
1893
|
+
return rows;
|
|
1894
|
+
}
|
|
1895
|
+
function runLegacyOrphanSweep({
|
|
1896
|
+
nowMs = Date.now(),
|
|
1897
|
+
protectedPids = [process.pid],
|
|
1898
|
+
listProcesses = listProcessesForSweep,
|
|
1899
|
+
killTree = killProcessTree,
|
|
1900
|
+
log = () => {
|
|
1901
|
+
}
|
|
1902
|
+
} = {}) {
|
|
1903
|
+
try {
|
|
1904
|
+
const processes = listProcesses({ nowMs });
|
|
1905
|
+
const { kills } = selectLegacyOrphans({
|
|
1906
|
+
processes,
|
|
1907
|
+
cutoffMs: nowMs - SWEEP_RECENCY_BUFFER_MS,
|
|
1908
|
+
protectedPids: new Set(protectedPids)
|
|
1909
|
+
});
|
|
1910
|
+
const killed = [];
|
|
1911
|
+
for (const kill of kills) {
|
|
1912
|
+
const done = killTree(kill.pid);
|
|
1913
|
+
log(`legacy-orphan-sweep ${done ? "killed" : "FAILED to kill"} pid=${kill.pid} sig=${kill.signature} cmd=${String(kill.commandLine).slice(0, 200)}`);
|
|
1914
|
+
if (done) killed.push({ pid: kill.pid, signature: kill.signature });
|
|
1915
|
+
}
|
|
1916
|
+
const failed2 = kills.length - killed.length;
|
|
1917
|
+
const detail = kills.length === 0 ? `no orphaned agent processes matched the sweep criteria (${processes.length} scanned)` : `purged ${killed.length} of ${kills.length} orphaned agent process tree(s)${failed2 > 0 ? ` (${failed2} kill(s) failed)` : ""}: ${killed.map((k) => `${k.pid}:${k.signature}`).join(", ").slice(0, 700)}`;
|
|
1918
|
+
return { ok: true, status: 0, detail, killed };
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
return { ok: false, status: 1, detail: `legacy sweep error: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), killed: [] };
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/runner/supervisor-activation.mjs
|
|
1925
|
+
var MAX_ACK_ATTEMPTS = 3;
|
|
1926
|
+
var delay = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1927
|
+
async function waitForAuthoritativeRunnerHeartbeat({
|
|
1928
|
+
client,
|
|
1929
|
+
runnerId: runnerId2,
|
|
1930
|
+
operatorId,
|
|
1931
|
+
runnerInstanceId,
|
|
1932
|
+
excludeRunnerInstanceId,
|
|
1933
|
+
daemonVersion,
|
|
1934
|
+
supervisorIdentity,
|
|
1935
|
+
timeoutMs = 6e4,
|
|
1936
|
+
pollMs = 1e3
|
|
1937
|
+
}) {
|
|
1938
|
+
if (!runnerInstanceId && !excludeRunnerInstanceId) throw new Error("runner instance identity proof unavailable");
|
|
1939
|
+
const deadline = Date.now() + timeoutMs;
|
|
1940
|
+
while (Date.now() < deadline) {
|
|
1941
|
+
try {
|
|
1942
|
+
const runners = await client.getRunnerStatus({ ...operatorId ? { operatorId } : {} });
|
|
1943
|
+
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));
|
|
1944
|
+
if (runner) return runner;
|
|
1945
|
+
} catch {
|
|
1946
|
+
}
|
|
1947
|
+
await delay(pollMs);
|
|
1948
|
+
}
|
|
1949
|
+
throw new Error("authoritative child heartbeat attestation timed out");
|
|
1950
|
+
}
|
|
1951
|
+
async function finishPendingActivation({
|
|
1952
|
+
client,
|
|
1953
|
+
child,
|
|
1954
|
+
runtimeRoot,
|
|
1955
|
+
operatorId,
|
|
1956
|
+
runnerId: runnerId2,
|
|
1957
|
+
selfPath: selfPath2,
|
|
1958
|
+
packageVersion: packageVersion2,
|
|
1959
|
+
supervisorIdentity,
|
|
1960
|
+
waitForLocalRunner: waitForLocalRunner2,
|
|
1961
|
+
localStatus: localStatus2,
|
|
1962
|
+
waitForCloudRunner = waitForAuthoritativeRunnerHeartbeat,
|
|
1963
|
+
cloudTimeoutMs,
|
|
1964
|
+
cloudPollMs,
|
|
1965
|
+
stopChild: stopChild2,
|
|
1966
|
+
launchPreviousChild,
|
|
1967
|
+
log = () => {
|
|
1968
|
+
}
|
|
1969
|
+
}) {
|
|
1970
|
+
const pointer = runtimeRoot ? readActivation(runtimeRoot) : null;
|
|
1971
|
+
if (!pointer?.pending) return true;
|
|
1972
|
+
const runnerIdForAction = pointer.pending.runner_id || runnerId2;
|
|
1973
|
+
const operatorIdForAction = pointer.pending.operator_id || operatorId;
|
|
1974
|
+
if (pointer.pending.terminal_status === "failed") {
|
|
1975
|
+
try {
|
|
1976
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
1977
|
+
runnerId: runnerIdForAction,
|
|
1978
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
1979
|
+
...supervisorIdentity,
|
|
1980
|
+
status: "failed",
|
|
1981
|
+
detail: pointer.pending.terminal_detail
|
|
1982
|
+
});
|
|
1983
|
+
acknowledgeActivationFailure(runtimeRoot, pointer);
|
|
1984
|
+
return true;
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
log(`activation failure acknowledgement remains pending: ${error instanceof Error ? error.message : String(error)}`);
|
|
1987
|
+
await stopChild2(child);
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
let attestation;
|
|
1992
|
+
try {
|
|
1993
|
+
attestation = attestCurrentSupervisor({ runtimeRoot, selfPath: selfPath2, version: packageVersion2 });
|
|
1994
|
+
if (!attestation.ok) throw new Error(attestation.detail);
|
|
1995
|
+
if (!await waitForLocalRunner2(child)) throw new Error("activated runner did not become locally ready");
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
let detail = `activation attestation failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 400);
|
|
1998
|
+
let rolledBack = null;
|
|
1999
|
+
try {
|
|
2000
|
+
rolledBack = rollbackActivation(runtimeRoot, pointer, detail);
|
|
2001
|
+
} catch (rollbackError) {
|
|
2002
|
+
detail = `${detail}; rollback pending: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`.slice(0, 400);
|
|
2003
|
+
}
|
|
2004
|
+
try {
|
|
2005
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
2006
|
+
runnerId: runnerIdForAction,
|
|
2007
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
2008
|
+
...supervisorIdentity,
|
|
2009
|
+
status: "failed",
|
|
2010
|
+
detail
|
|
2011
|
+
});
|
|
2012
|
+
if (rolledBack) acknowledgeActivationFailure(runtimeRoot, rolledBack);
|
|
2013
|
+
} catch {
|
|
2014
|
+
} finally {
|
|
2015
|
+
await stopChild2(child);
|
|
2016
|
+
}
|
|
2017
|
+
return false;
|
|
2018
|
+
}
|
|
2019
|
+
let activatedRunnerInstanceId = null;
|
|
2020
|
+
try {
|
|
2021
|
+
const status = await localStatus2();
|
|
2022
|
+
activatedRunnerInstanceId = status?.runnerInstanceId || null;
|
|
2023
|
+
await waitForCloudRunner({
|
|
2024
|
+
client,
|
|
2025
|
+
runnerId: runnerIdForAction,
|
|
2026
|
+
operatorId: operatorIdForAction,
|
|
2027
|
+
runnerInstanceId: status?.runnerInstanceId,
|
|
2028
|
+
daemonVersion: `vo-mcp/${attestation.active.version}`,
|
|
2029
|
+
supervisorIdentity,
|
|
2030
|
+
timeoutMs: cloudTimeoutMs,
|
|
2031
|
+
pollMs: cloudPollMs
|
|
2032
|
+
});
|
|
2033
|
+
await client.completeRunnerControl(pointer.pending.action_id, {
|
|
2034
|
+
runnerId: runnerIdForAction,
|
|
2035
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
2036
|
+
...supervisorIdentity,
|
|
2037
|
+
status: "succeeded",
|
|
2038
|
+
detail: `attested new supervisor ${attestation.active.version} ${attestation.active.integrity}`
|
|
2039
|
+
});
|
|
2040
|
+
finalizeActivation(runtimeRoot, attestation.pointer);
|
|
2041
|
+
return true;
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
const failure = `activation acknowledgement failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 400);
|
|
2044
|
+
let retry;
|
|
2045
|
+
try {
|
|
2046
|
+
retry = recordActivationRetry(runtimeRoot, attestation.pointer, failure);
|
|
2047
|
+
} catch (retryError) {
|
|
2048
|
+
log(`activation retry could not be recorded: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
|
|
2049
|
+
await stopChild2(child);
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
2052
|
+
if (retry.pending.ack_attempts < MAX_ACK_ATTEMPTS) {
|
|
2053
|
+
log(`activation acknowledgement pending (${retry.pending.ack_attempts}/${MAX_ACK_ATTEMPTS})`);
|
|
2054
|
+
await stopChild2(child);
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
const previous = validateSlot(runtimeRoot, retry.previous);
|
|
2058
|
+
let detail = `${failure}; retry limit reached; previous runtime restored`.slice(0, 400);
|
|
2059
|
+
let rollbackChild = null;
|
|
2060
|
+
let rolledBack = null;
|
|
2061
|
+
try {
|
|
2062
|
+
if (!previous.ok) throw new Error(`previous runtime invalid: ${previous.detail}`, { cause: error });
|
|
2063
|
+
rolledBack = rollbackActivation(runtimeRoot, retry, detail);
|
|
2064
|
+
await stopChild2(child);
|
|
2065
|
+
rollbackChild = launchPreviousChild(previous.paths.entry);
|
|
2066
|
+
if (!await waitForLocalRunner2(rollbackChild)) throw new Error("previous runner did not become locally ready", { cause: error });
|
|
2067
|
+
const status = await localStatus2();
|
|
2068
|
+
await waitForCloudRunner({
|
|
2069
|
+
client,
|
|
2070
|
+
runnerId: runnerIdForAction,
|
|
2071
|
+
operatorId: operatorIdForAction,
|
|
2072
|
+
runnerInstanceId: status?.runnerInstanceId,
|
|
2073
|
+
excludeRunnerInstanceId: status?.runnerInstanceId ? void 0 : activatedRunnerInstanceId,
|
|
2074
|
+
daemonVersion: `vo-mcp/${retry.previous.version}`,
|
|
2075
|
+
supervisorIdentity,
|
|
2076
|
+
timeoutMs: cloudTimeoutMs,
|
|
2077
|
+
pollMs: cloudPollMs
|
|
2078
|
+
});
|
|
2079
|
+
} catch (rollbackError) {
|
|
2080
|
+
detail = `${detail}; rollback proof failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`.slice(0, 400);
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
await client.completeRunnerControl(retry.pending.action_id, {
|
|
2084
|
+
runnerId: runnerIdForAction,
|
|
2085
|
+
...operatorIdForAction ? { operatorId: operatorIdForAction } : {},
|
|
2086
|
+
...supervisorIdentity,
|
|
2087
|
+
status: "failed",
|
|
2088
|
+
detail
|
|
2089
|
+
});
|
|
2090
|
+
if (rolledBack) acknowledgeActivationFailure(runtimeRoot, rolledBack);
|
|
2091
|
+
} catch {
|
|
2092
|
+
}
|
|
2093
|
+
if (rollbackChild) await stopChild2(rollbackChild);
|
|
2094
|
+
else await stopChild2(child);
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/runner/supervisor-child-entry.mjs
|
|
2100
|
+
var VERSION_RE2 = /^(?:[\w.-]+\/)?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u;
|
|
2101
|
+
function parseRuntimeVersion(value) {
|
|
2102
|
+
if (typeof value !== "string") return null;
|
|
2103
|
+
const match = VERSION_RE2.exec(value.trim());
|
|
2104
|
+
if (!match) return null;
|
|
2105
|
+
const prerelease = match[4] ? match[4].split(".").map((part) => /^\d+$/u.test(part) ? Number(part) : part) : null;
|
|
2106
|
+
return { release: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease };
|
|
2107
|
+
}
|
|
2108
|
+
function compareRuntimeVersions(a, b) {
|
|
2109
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2110
|
+
const av = a.release[i] ?? 0;
|
|
2111
|
+
const bv = b.release[i] ?? 0;
|
|
2112
|
+
if (av !== bv) return av < bv ? -1 : 1;
|
|
2113
|
+
}
|
|
2114
|
+
if (!a.prerelease && !b.prerelease) return 0;
|
|
2115
|
+
if (!a.prerelease) return 1;
|
|
2116
|
+
if (!b.prerelease) return -1;
|
|
2117
|
+
const len = Math.max(a.prerelease.length, b.prerelease.length);
|
|
2118
|
+
for (let i = 0; i < len; i += 1) {
|
|
2119
|
+
const av = a.prerelease[i];
|
|
2120
|
+
const bv = b.prerelease[i];
|
|
2121
|
+
if (av === void 0) return -1;
|
|
2122
|
+
if (bv === void 0) return 1;
|
|
2123
|
+
if (av === bv) continue;
|
|
2124
|
+
const aNum = typeof av === "number";
|
|
2125
|
+
const bNum = typeof bv === "number";
|
|
2126
|
+
if (aNum && bNum) return av < bv ? -1 : 1;
|
|
2127
|
+
if (aNum !== bNum) return aNum ? -1 : 1;
|
|
2128
|
+
return String(av) < String(bv) ? -1 : 1;
|
|
2129
|
+
}
|
|
2130
|
+
return 0;
|
|
2131
|
+
}
|
|
2132
|
+
function resolveSupervisorChildEntry({
|
|
2133
|
+
runtimeRoot,
|
|
2134
|
+
bundledEntry,
|
|
2135
|
+
bundledVersion,
|
|
2136
|
+
readPointer = readActivation,
|
|
2137
|
+
validate = validateSlot
|
|
2138
|
+
}) {
|
|
2139
|
+
const bundled = (detail) => ({
|
|
2140
|
+
// The bundled entry is `dist/runner-cli.js`, which IS the daemon and takes
|
|
2141
|
+
// no subcommand. The slot entry is `bin/vo-mcp`, the multiplexed CLI, which
|
|
2142
|
+
// needs the `runner` subcommand — the same argv the proven rollback path
|
|
2143
|
+
// (`launchPreviousChild`) uses.
|
|
2144
|
+
args: [bundledEntry],
|
|
2145
|
+
entry: bundledEntry,
|
|
2146
|
+
source: "bundled",
|
|
2147
|
+
version: String(bundledVersion || "unknown"),
|
|
2148
|
+
descriptor: `bundled ${bundledVersion || "unknown"} ${bundledEntry}`,
|
|
2149
|
+
detail
|
|
2150
|
+
});
|
|
2151
|
+
if (!runtimeRoot) return bundled("no runtime root");
|
|
2152
|
+
let pointer;
|
|
2153
|
+
try {
|
|
2154
|
+
pointer = readPointer(runtimeRoot);
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
return bundled(`activation pointer unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2157
|
+
}
|
|
2158
|
+
if (!pointer?.active) return bundled("no activated runtime slot");
|
|
2159
|
+
if (pointer.pending) return bundled("runtime activation still pending");
|
|
2160
|
+
const activeVersion = parseRuntimeVersion(pointer.active.version);
|
|
2161
|
+
const currentVersion = parseRuntimeVersion(bundledVersion);
|
|
2162
|
+
if (!activeVersion) return bundled(`active slot version unparseable: ${String(pointer.active.version)}`);
|
|
2163
|
+
if (!currentVersion) return bundled(`bundled version unparseable: ${String(bundledVersion)}`);
|
|
2164
|
+
if (compareRuntimeVersions(activeVersion, currentVersion) <= 0) {
|
|
2165
|
+
return bundled(`active slot ${pointer.active.version} is not newer than bundled ${bundledVersion}`);
|
|
2166
|
+
}
|
|
2167
|
+
let validated;
|
|
2168
|
+
try {
|
|
2169
|
+
validated = validate(runtimeRoot, pointer.active);
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
return bundled(`slot validation threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
2172
|
+
}
|
|
2173
|
+
if (!validated?.ok) return bundled(`active slot invalid: ${validated?.detail || "unknown"}`);
|
|
2174
|
+
const entry = validated.paths.entry;
|
|
2175
|
+
return {
|
|
2176
|
+
args: [entry, "runner"],
|
|
2177
|
+
entry,
|
|
2178
|
+
source: "active-slot",
|
|
2179
|
+
version: pointer.active.version,
|
|
2180
|
+
descriptor: `active-slot ${pointer.active.version} ${entry}`,
|
|
2181
|
+
detail: `activated slot ${pointer.active.slot_id}`
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// src/runner/supervisor-child-env.mjs
|
|
2186
|
+
import { hostname as systemHostname } from "node:os";
|
|
2187
|
+
|
|
2188
|
+
// src/runner-readiness.mjs
|
|
2189
|
+
function failed({ paired = false, operatorId = null, tenantId = null, githubReady = null, error, message }) {
|
|
2190
|
+
return { ok: false, paired, operatorId, tenantId, githubReady, error, message };
|
|
2191
|
+
}
|
|
2192
|
+
async function responseBody(response) {
|
|
2193
|
+
try {
|
|
2194
|
+
const value = await response.json();
|
|
2195
|
+
return value && typeof value === "object" ? value : {};
|
|
2196
|
+
} catch {
|
|
2197
|
+
return {};
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
function serverMessage(body, fallback) {
|
|
2201
|
+
return typeof body.message === "string" && body.message.trim() ? body.message.trim() : fallback;
|
|
2202
|
+
}
|
|
2203
|
+
async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
|
|
2204
|
+
const controller = new AbortController();
|
|
2205
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2206
|
+
try {
|
|
2207
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
2208
|
+
} finally {
|
|
2209
|
+
clearTimeout(timer);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async function probeRunnerReadiness({
|
|
2213
|
+
controlPlaneUrl,
|
|
2214
|
+
token,
|
|
2215
|
+
fetchImpl = fetch,
|
|
2216
|
+
requireGithub = false,
|
|
2217
|
+
timeoutMs = 1e4
|
|
2218
|
+
}) {
|
|
2219
|
+
const base = controlPlaneUrl.replace(/\/+$/u, "");
|
|
2220
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
2221
|
+
let identityResponse;
|
|
2222
|
+
try {
|
|
2223
|
+
identityResponse = await fetchWithTimeout(
|
|
2224
|
+
fetchImpl,
|
|
2225
|
+
`${base}/api/v1/auth/me`,
|
|
2226
|
+
{ headers },
|
|
2227
|
+
timeoutMs
|
|
2228
|
+
);
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2231
|
+
return failed({
|
|
2232
|
+
error: "control_plane_unreachable",
|
|
2233
|
+
message: `AlgoHQ could not be reached: ${detail}`
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
const identity = await responseBody(identityResponse);
|
|
2237
|
+
if (!identityResponse.ok) {
|
|
2238
|
+
return failed({
|
|
2239
|
+
error: "credential_rejected",
|
|
2240
|
+
message: "The saved pairing is expired or revoked. Pair this computer again."
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (identity.provisioned !== true || identity.role !== "operator" || typeof identity.operator_id !== "string" || !identity.operator_id.trim() || typeof identity.tenant_id !== "string" || !identity.tenant_id.trim()) {
|
|
2244
|
+
return failed({
|
|
2245
|
+
error: "operator_identity_missing",
|
|
2246
|
+
message: "The pairing credential is valid but has no operator identity. Pair this computer again."
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
const operatorId = identity.operator_id.trim();
|
|
2250
|
+
const tenantId = identity.tenant_id.trim();
|
|
2251
|
+
if (!requireGithub) {
|
|
2252
|
+
return {
|
|
2253
|
+
ok: true,
|
|
2254
|
+
paired: true,
|
|
2255
|
+
operatorId,
|
|
2256
|
+
tenantId,
|
|
2257
|
+
githubReady: null,
|
|
2258
|
+
error: null,
|
|
2259
|
+
message: "Paired to AlgoHQ."
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
let githubResponse;
|
|
2263
|
+
try {
|
|
2264
|
+
githubResponse = await fetchWithTimeout(
|
|
2265
|
+
fetchImpl,
|
|
2266
|
+
`${base}/api/v1/github/installation-token`,
|
|
2267
|
+
{
|
|
2268
|
+
method: "POST",
|
|
2269
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
2270
|
+
body: "{}"
|
|
2271
|
+
},
|
|
2272
|
+
timeoutMs
|
|
2273
|
+
);
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2276
|
+
return failed({
|
|
2277
|
+
paired: true,
|
|
2278
|
+
operatorId,
|
|
2279
|
+
tenantId,
|
|
2280
|
+
githubReady: false,
|
|
2281
|
+
error: "github_preflight_unreachable",
|
|
2282
|
+
message: `GitHub publication readiness could not be checked: ${detail}`
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
const github = await responseBody(githubResponse);
|
|
2286
|
+
if (!githubResponse.ok || typeof github.token !== "string" || !github.token) {
|
|
2287
|
+
const error = typeof github.error === "string" && github.error ? github.error : "github_not_ready";
|
|
2288
|
+
return failed({
|
|
2289
|
+
paired: true,
|
|
2290
|
+
operatorId,
|
|
2291
|
+
tenantId,
|
|
2292
|
+
githubReady: false,
|
|
2293
|
+
error,
|
|
2294
|
+
message: serverMessage(github, `GitHub publication preflight failed (HTTP ${githubResponse.status}).`)
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
ok: true,
|
|
2299
|
+
paired: true,
|
|
2300
|
+
operatorId,
|
|
2301
|
+
tenantId,
|
|
2302
|
+
githubReady: true,
|
|
2303
|
+
error: null,
|
|
2304
|
+
message: "Paired and ready to publish through the Algosuite GitHub App."
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function pairedOperatorScope(readiness) {
|
|
2308
|
+
return readiness?.ok === true && readiness?.paired === true && typeof readiness.operatorId === "string" && readiness.operatorId.trim() ? readiness.operatorId.trim() : null;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/runner/supervisor-child-env.mjs
|
|
2312
|
+
function resolveSupervisorRunnerId(env = {}, hostname = systemHostname) {
|
|
2313
|
+
const explicit = String(env.VO_CODE_RUNNER_ID || "").trim();
|
|
2314
|
+
return explicit || `vo-code-runner-${hostname()}`;
|
|
2315
|
+
}
|
|
2316
|
+
function buildSupervisorChildEnv({
|
|
2317
|
+
baseEnv = {},
|
|
2318
|
+
controlPlaneUrl,
|
|
2319
|
+
explicitAdminToken = null,
|
|
2320
|
+
pairedOperatorId = null
|
|
2321
|
+
} = {}) {
|
|
2322
|
+
const childEnv = {
|
|
2323
|
+
...baseEnv,
|
|
2324
|
+
VO_CONTROL_PLANE_URL: controlPlaneUrl
|
|
2325
|
+
};
|
|
2326
|
+
const adminToken = typeof explicitAdminToken === "string" ? explicitAdminToken.trim() : "";
|
|
2327
|
+
const operatorId = typeof pairedOperatorId === "string" ? pairedOperatorId.trim() : "";
|
|
2328
|
+
if (adminToken) childEnv.VO_CONTROL_PLANE_ADMIN_TOKEN = adminToken;
|
|
2329
|
+
else delete childEnv.VO_CONTROL_PLANE_ADMIN_TOKEN;
|
|
2330
|
+
if (operatorId) childEnv.VO_CODE_RUNNER_OPERATOR_IDS = operatorId;
|
|
2331
|
+
return childEnv;
|
|
2332
|
+
}
|
|
2333
|
+
async function prepareSupervisorAuth({
|
|
2334
|
+
baseEnv = {},
|
|
2335
|
+
storedCredential = null,
|
|
2336
|
+
controlPlaneUrl,
|
|
2337
|
+
probeReadiness = probeRunnerReadiness
|
|
2338
|
+
} = {}) {
|
|
2339
|
+
const explicitAdminToken = baseEnv.VO_CONTROL_PLANE_ADMIN_TOKEN?.trim();
|
|
2340
|
+
const token = explicitAdminToken || storedCredential?.vo_credential;
|
|
2341
|
+
if (!token) throw new Error("runner is not paired; run `vo-mcp pair` once on this host");
|
|
2342
|
+
let operatorId = String(baseEnv.VO_CODE_RUNNER_OPERATOR_IDS || "").split(/[\s,]+/u).filter(Boolean)[0] || void 0;
|
|
2343
|
+
if (!explicitAdminToken) {
|
|
2344
|
+
const readiness = await probeReadiness({ controlPlaneUrl, token, requireGithub: true });
|
|
2345
|
+
if (!readiness.ok) throw new Error(`runner readiness failed: ${readiness.message}`);
|
|
2346
|
+
operatorId = pairedOperatorScope(readiness) || void 0;
|
|
2347
|
+
if (!operatorId) throw new Error("runner readiness failed: paired operator scope is missing");
|
|
2348
|
+
}
|
|
2349
|
+
const childEnv = buildSupervisorChildEnv({
|
|
2350
|
+
baseEnv,
|
|
2351
|
+
controlPlaneUrl,
|
|
2352
|
+
explicitAdminToken,
|
|
2353
|
+
pairedOperatorId: explicitAdminToken ? null : operatorId
|
|
2354
|
+
});
|
|
2355
|
+
const clientEnv = {
|
|
2356
|
+
...baseEnv,
|
|
2357
|
+
VO_CONTROL_PLANE_ADMIN_TOKEN: token,
|
|
2358
|
+
VO_CONTROL_PLANE_URL: controlPlaneUrl,
|
|
2359
|
+
...operatorId ? { VO_CODE_RUNNER_OPERATOR_IDS: operatorId } : {}
|
|
2360
|
+
};
|
|
2361
|
+
return { childEnv, clientEnv, operatorId };
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// src/runner/supervisor-credential-reader.mjs
|
|
2365
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2366
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2367
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2368
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2369
|
+
function defaultCredentialHelperPath(metaUrl = import.meta.url) {
|
|
2370
|
+
const moduleDir = dirname3(fileURLToPath2(metaUrl));
|
|
2371
|
+
const bundled = join4(moduleDir, "supervisor-credential-helper.js");
|
|
2372
|
+
const source = join4(moduleDir, "..", "supervisor-credential-helper.mjs");
|
|
2373
|
+
return existsSync6(source) ? source : bundled;
|
|
2374
|
+
}
|
|
2375
|
+
function readStoredCredentialIsolated({
|
|
2376
|
+
spawn: spawn2 = spawnSync5,
|
|
2377
|
+
execPath = process.execPath,
|
|
2378
|
+
helperPath = defaultCredentialHelperPath(),
|
|
2379
|
+
helperArgs = [],
|
|
2380
|
+
env = process.env
|
|
2381
|
+
} = {}) {
|
|
2382
|
+
const result = spawn2(execPath, [helperPath, ...helperArgs], {
|
|
2383
|
+
env,
|
|
2384
|
+
encoding: "utf8",
|
|
2385
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2386
|
+
shell: false,
|
|
2387
|
+
windowsHide: true,
|
|
2388
|
+
timeout: 1e4
|
|
2389
|
+
});
|
|
2390
|
+
if (result.status !== 0) {
|
|
2391
|
+
throw new Error("runner credential helper could not read the paired credential");
|
|
2392
|
+
}
|
|
2393
|
+
try {
|
|
2394
|
+
const parsed = JSON.parse(String(result.stdout || ""));
|
|
2395
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
2396
|
+
} catch {
|
|
2397
|
+
throw new Error("runner credential helper returned an invalid credential");
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/runner-supervisor.mjs
|
|
2402
|
+
var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
2403
|
+
var POLL_MS = 5e3;
|
|
2404
|
+
var CHILD_START_MS = 1500;
|
|
2405
|
+
var SUPERVISOR_CAPABILITIES = ["bundled-runtime-slots-v1", "legacy-orphan-purge-v1"];
|
|
2406
|
+
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;
|
|
2407
|
+
var selfPath = fileURLToPath3(import.meta.url);
|
|
2408
|
+
var bundledChildEntry = join5(dirname4(selfPath), "runner-cli.js");
|
|
2409
|
+
var supervisorRuntimeRoot = runtimeRootFromEnv(process.env);
|
|
2410
|
+
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2411
|
+
var runnerId = resolveSupervisorRunnerId(process.env);
|
|
2412
|
+
function packageVersion() {
|
|
2413
|
+
try {
|
|
2414
|
+
return createRequire(import.meta.url)("../package.json").version || "unknown";
|
|
2415
|
+
} catch {
|
|
2416
|
+
return "unknown";
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
var requestedSupervisorInstanceId = UUID_RE2.test(String(process.env.VO_RUNNER_SUPERVISOR_INSTANCE_ID || "")) ? process.env.VO_RUNNER_SUPERVISOR_INSTANCE_ID : randomUUID3();
|
|
2420
|
+
var supervisorInstanceId = activationSupervisorInstanceId(
|
|
2421
|
+
runtimeRootFromEnv(process.env),
|
|
2422
|
+
requestedSupervisorInstanceId
|
|
2423
|
+
);
|
|
2424
|
+
var supervisorVersion = packageVersion();
|
|
2425
|
+
var supervisorControlIdentity = {
|
|
2426
|
+
supervisorInstanceId,
|
|
2427
|
+
supervisorVersion,
|
|
2428
|
+
capabilities: SUPERVISOR_CAPABILITIES
|
|
2429
|
+
};
|
|
2430
|
+
var lastResolvedChildDescriptor = null;
|
|
2431
|
+
function spawnChild(childEnv) {
|
|
2432
|
+
const resolved = resolveSupervisorChildEntry({
|
|
2433
|
+
runtimeRoot: supervisorRuntimeRoot,
|
|
2434
|
+
bundledEntry: bundledChildEntry,
|
|
2435
|
+
bundledVersion: supervisorVersion
|
|
2436
|
+
});
|
|
2437
|
+
if (resolved.descriptor !== lastResolvedChildDescriptor) {
|
|
2438
|
+
console.error(`[vo-runner supervisor] child entry -> ${resolved.descriptor} (${resolved.detail})`);
|
|
2439
|
+
lastResolvedChildDescriptor = resolved.descriptor;
|
|
2440
|
+
}
|
|
2441
|
+
return spawn(process.execPath, resolved.args, {
|
|
2442
|
+
env: childEnv,
|
|
2443
|
+
stdio: "inherit",
|
|
2444
|
+
windowsHide: true
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
function spawnPreviousChild(entry, childEnv) {
|
|
2448
|
+
return spawn(process.execPath, [entry, "runner"], {
|
|
2449
|
+
env: childEnv,
|
|
2450
|
+
stdio: "inherit",
|
|
2451
|
+
windowsHide: true
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
async function localStatus() {
|
|
2455
|
+
try {
|
|
2456
|
+
const port = Number(process.env.VO_CODE_RUNNER_CONTROL_PORT || 7787);
|
|
2457
|
+
const response = await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(800) });
|
|
2458
|
+
if (!response.ok) return null;
|
|
2459
|
+
const body = await response.json();
|
|
2460
|
+
return body?.ok === true ? body : null;
|
|
2461
|
+
} catch {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async function waitForLocalRunner(child) {
|
|
2466
|
+
const deadline = Date.now() + 15e3;
|
|
2467
|
+
while (Date.now() < deadline) {
|
|
2468
|
+
if (child.exitCode !== null) return false;
|
|
2469
|
+
const status = await localStatus();
|
|
2470
|
+
if (status?.running === true && Number(status.pid) === Number(child.pid)) return true;
|
|
2471
|
+
await sleep(500);
|
|
2472
|
+
}
|
|
2473
|
+
return false;
|
|
2474
|
+
}
|
|
2475
|
+
async function stopChild(child) {
|
|
2476
|
+
if (!child || child.exitCode !== null) return;
|
|
2477
|
+
child.kill(process.platform === "win32" ? void 0 : "SIGTERM");
|
|
2478
|
+
await Promise.race([
|
|
2479
|
+
new Promise((resolve5) => child.once("exit", resolve5)),
|
|
2480
|
+
sleep(15e3)
|
|
2481
|
+
]);
|
|
2482
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
2483
|
+
}
|
|
2484
|
+
async function main() {
|
|
2485
|
+
const stored = readStoredCredentialIsolated();
|
|
2486
|
+
const controlPlaneUrl = process.env.VO_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL;
|
|
2487
|
+
const { childEnv, clientEnv, operatorId } = await prepareSupervisorAuth({
|
|
2488
|
+
baseEnv: process.env,
|
|
2489
|
+
storedCredential: stored,
|
|
2490
|
+
controlPlaneUrl
|
|
2491
|
+
});
|
|
2492
|
+
Object.assign(childEnv, {
|
|
2493
|
+
VO_RUNNER_SUPERVISOR_INSTANCE_ID: supervisorInstanceId,
|
|
2494
|
+
VO_RUNNER_SUPERVISOR_VERSION: supervisorVersion,
|
|
2495
|
+
VO_RUNNER_SUPERVISOR_CAPABILITIES: SUPERVISOR_CAPABILITIES.join(",")
|
|
2496
|
+
});
|
|
2497
|
+
const client = createControlPlaneClient({ baseUrl: controlPlaneUrl, env: clientEnv });
|
|
2498
|
+
const runtimeRoot = runtimeRootFromEnv(process.env);
|
|
2499
|
+
let child = null;
|
|
2500
|
+
let stopping = false;
|
|
2501
|
+
let handling = false;
|
|
2502
|
+
let degraded = false;
|
|
2503
|
+
const respawn = () => {
|
|
2504
|
+
if (!stopping && !degraded && !handling && (!child || child.exitCode !== null)) child = launchChild();
|
|
2505
|
+
};
|
|
2506
|
+
const launchChild = () => {
|
|
2507
|
+
const next = spawnChild(childEnv);
|
|
2508
|
+
next.on("exit", () => setTimeout(respawn, 2e3));
|
|
2509
|
+
return next;
|
|
2510
|
+
};
|
|
2511
|
+
child = launchChild();
|
|
2512
|
+
if (!await finishPendingActivation({
|
|
2513
|
+
client,
|
|
2514
|
+
child,
|
|
2515
|
+
runtimeRoot,
|
|
2516
|
+
operatorId,
|
|
2517
|
+
runnerId,
|
|
2518
|
+
selfPath,
|
|
2519
|
+
packageVersion: supervisorVersion,
|
|
2520
|
+
supervisorIdentity: supervisorControlIdentity,
|
|
2521
|
+
waitForLocalRunner,
|
|
2522
|
+
localStatus,
|
|
2523
|
+
stopChild,
|
|
2524
|
+
launchPreviousChild: (entry) => spawnPreviousChild(entry, childEnv),
|
|
2525
|
+
log: (message) => console.error(`[vo-runner supervisor] ${message}`)
|
|
2526
|
+
})) {
|
|
2527
|
+
degraded = true;
|
|
2528
|
+
process.exitCode = 1;
|
|
2529
|
+
await stopChild(child);
|
|
2530
|
+
child = null;
|
|
2531
|
+
console.error("[vo-runner supervisor] activation FAILED \u2014 entering degraded mode: not serving tasks, still polling for remote control actions");
|
|
2532
|
+
}
|
|
2533
|
+
const shutdown = async () => {
|
|
2534
|
+
if (stopping) return;
|
|
2535
|
+
stopping = true;
|
|
2536
|
+
await stopChild(child);
|
|
2537
|
+
};
|
|
2538
|
+
process.on("SIGINT", () => {
|
|
2539
|
+
void shutdown().finally(() => process.exit(0));
|
|
2540
|
+
});
|
|
2541
|
+
process.on("SIGTERM", () => {
|
|
2542
|
+
void shutdown().finally(() => process.exit(0));
|
|
2543
|
+
});
|
|
2544
|
+
while (!stopping) {
|
|
2545
|
+
try {
|
|
2546
|
+
const action = await client.pollRunnerControl({
|
|
2547
|
+
runnerId,
|
|
2548
|
+
...operatorId ? { operatorId } : {},
|
|
2549
|
+
...supervisorControlIdentity
|
|
2550
|
+
});
|
|
2551
|
+
if (!action) {
|
|
2552
|
+
await sleep(POLL_MS);
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
handling = true;
|
|
2556
|
+
const beforeStop = await localStatus();
|
|
2557
|
+
if (beforeStop && Number(beforeStop.activeTasks || 0) > 0) {
|
|
2558
|
+
await client.completeRunnerControl(action.actionId, {
|
|
2559
|
+
runnerId,
|
|
2560
|
+
...operatorId ? { operatorId } : {},
|
|
2561
|
+
...supervisorControlIdentity,
|
|
2562
|
+
status: "failed",
|
|
2563
|
+
detail: `deferred safely: ${beforeStop.activeTasks} active task(s); retry when the runner is idle`
|
|
2564
|
+
});
|
|
2565
|
+
handling = false;
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
await stopChild(child);
|
|
2569
|
+
const bundledAction = action.kind === "update" || action.kind === "reinstall";
|
|
2570
|
+
const result = bundledAction ? stageAndActivateBundledUpdate({
|
|
2571
|
+
runtimeRoot,
|
|
2572
|
+
packageSpec: `@algosuite/vo-mcp@${action.desired_package_version}`,
|
|
2573
|
+
expectedVersion: action.desired_package_version,
|
|
2574
|
+
expectedIntegrity: action.desired_package_integrity,
|
|
2575
|
+
action: {
|
|
2576
|
+
actionId: action.actionId,
|
|
2577
|
+
runnerId,
|
|
2578
|
+
operatorId: operatorId || "",
|
|
2579
|
+
supervisorInstanceId
|
|
2580
|
+
},
|
|
2581
|
+
env: process.env,
|
|
2582
|
+
force: action.kind === "reinstall"
|
|
2583
|
+
}) : action.kind === "purge-orphans" ? runLegacyOrphanSweep({
|
|
2584
|
+
protectedPids: [process.pid],
|
|
2585
|
+
log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
|
|
2586
|
+
}) : runHostMaintenance(action.kind, {
|
|
2587
|
+
env: clientEnv,
|
|
2588
|
+
log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
|
|
2589
|
+
});
|
|
2590
|
+
if (result.ok && result.handoff) {
|
|
2591
|
+
console.warn(`[vo-runner supervisor] activated ${result.active.version}; exiting for new-process attestation`);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
child = launchChild();
|
|
2595
|
+
await sleep(CHILD_START_MS);
|
|
2596
|
+
if (child.exitCode !== null || !await waitForLocalRunner(child)) result.ok = false;
|
|
2597
|
+
else degraded = false;
|
|
2598
|
+
await client.completeRunnerControl(action.actionId, {
|
|
2599
|
+
runnerId,
|
|
2600
|
+
...operatorId ? { operatorId } : {},
|
|
2601
|
+
...supervisorControlIdentity,
|
|
2602
|
+
status: result.ok ? "succeeded" : "failed",
|
|
2603
|
+
detail: result.ok ? result.detail ? `${result.detail}; runner ${packageVersion()} reconnected`.slice(0, 1e3) : `runner ${packageVersion()} reconnected` : `maintenance exited ${result.status}${result.detail ? `: ${result.detail}` : ""}`
|
|
2604
|
+
});
|
|
2605
|
+
handling = false;
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
console.error(`[vo-runner supervisor] ${error instanceof Error ? error.message : String(error)}`);
|
|
2608
|
+
handling = false;
|
|
2609
|
+
await sleep(POLL_MS);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
main().catch((error) => {
|
|
2614
|
+
console.error(`[vo-runner supervisor] fatal: ${error instanceof Error ? error.message : String(error)}`);
|
|
2615
|
+
process.exitCode = 1;
|
|
2616
|
+
});
|
|
2617
|
+
//# sourceMappingURL=runner-supervisor.js.map
|