@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.8

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.
@@ -0,0 +1,777 @@
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 { createRequire as createRequire2 } from "node:module";
35
+ import { fileURLToPath } from "node:url";
36
+ import { dirname as dirname2, join as join2 } from "node:path";
37
+
38
+ // ../../scripts/virtual-office/code-runner/control-plane-client.mjs
39
+ var cachedFirebaseToken = null;
40
+ async function resolveBearer(env) {
41
+ const adminToken = env.VO_CONTROL_PLANE_ADMIN_TOKEN;
42
+ if (adminToken) return adminToken;
43
+ if (cachedFirebaseToken) return cachedFirebaseToken;
44
+ const { getFirebaseAuth: getFirebaseAuth2 } = await Promise.resolve().then(() => (init_control_plane_auth_stub(), control_plane_auth_stub_exports));
45
+ const auth = await getFirebaseAuth2({ env });
46
+ if (!auth || !auth.idToken) {
47
+ throw new Error(
48
+ "no control-plane credential: set VO_CONTROL_PLANE_ADMIN_TOKEN, or SMOKE_EMAIL/SMOKE_PASSWORD/SMOKE_API_KEY"
49
+ );
50
+ }
51
+ cachedFirebaseToken = auth.idToken;
52
+ return cachedFirebaseToken;
53
+ }
54
+ function createControlPlaneClient({
55
+ baseUrl = process.env.VO_CONTROL_PLANE_URL || "",
56
+ env = process.env,
57
+ fetchImpl = fetch
58
+ } = {}) {
59
+ if (!baseUrl) {
60
+ throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
61
+ }
62
+ const root = baseUrl.replace(/\/+$/, "");
63
+ async function req(method, path, body) {
64
+ const bearer = await resolveBearer(env);
65
+ return fetchImpl(`${root}${path}`, {
66
+ method,
67
+ headers: {
68
+ "content-type": "application/json",
69
+ authorization: `Bearer ${bearer}`
70
+ },
71
+ body: body === void 0 ? void 0 : JSON.stringify(body)
72
+ });
73
+ }
74
+ return {
75
+ /**
76
+ * Claim the next pending task. Returns the task or null (empty queue).
77
+ * `repos` (optional `owner/name` list) and `operatorIds` (optional
78
+ * `operator_id` list) scope the claim so this daemon only picks up tasks it
79
+ * serves — the control-plane filters by both (logical AND), so another
80
+ * operator's task never lands on (or bills) this machine.
81
+ */
82
+ async claim(runnerId2, repos, operatorIds, session = {}) {
83
+ const body = { runner_id: runnerId2 };
84
+ if (Array.isArray(repos) && repos.length > 0) body.repos = repos;
85
+ if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
86
+ if (session.runnerInstanceId) body.runner_instance_id = session.runnerInstanceId;
87
+ if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
88
+ const res = await req("POST", "/api/v1/code-task/claim", body);
89
+ if (res.status === 401) {
90
+ cachedFirebaseToken = null;
91
+ throw new Error("claim unauthorized (401)");
92
+ }
93
+ if (!res.ok) throw new Error(`claim failed: HTTP ${res.status}`);
94
+ const json = await res.json();
95
+ return json && json.task ? json.task : null;
96
+ },
97
+ /**
98
+ * Enqueue a new code-task (used by the PR watcher to auto-dispatch a CI fix).
99
+ * Server derives operator/tenant from the daemon's authenticated principal.
100
+ * Returns the created task, or throws on a non-2xx response.
101
+ */
102
+ async enqueueCodeTask({ repo, prompt, max_budget_usd, max_turns }) {
103
+ const body = { repo, prompt };
104
+ if (typeof max_budget_usd === "number") body.max_budget_usd = max_budget_usd;
105
+ if (typeof max_turns === "number") body.max_turns = max_turns;
106
+ const res = await req("POST", "/api/v1/code-task", body);
107
+ if (res.status === 401) {
108
+ cachedFirebaseToken = null;
109
+ throw new Error("enqueue unauthorized (401)");
110
+ }
111
+ if (!res.ok) throw new Error(`enqueue failed: HTTP ${res.status}`);
112
+ const json = await res.json();
113
+ return json && json.task ? json.task : null;
114
+ },
115
+ /**
116
+ * Resume a failed/cancelled/max-turn partial code-task. The PR watcher uses
117
+ * this after the runner opens a partial draft PR and CI is no longer pending.
118
+ */
119
+ async resumeCodeTask(taskId) {
120
+ const res = await req("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/resume`, {});
121
+ if (res.status === 401) {
122
+ cachedFirebaseToken = null;
123
+ throw new Error("resume unauthorized (401)");
124
+ }
125
+ if (!res.ok) throw new Error(`resume failed: HTTP ${res.status}`);
126
+ const json = await res.json();
127
+ return json && json.task ? json.task : null;
128
+ },
129
+ /**
130
+ * Send a CI-green PR through the production verify-before-act merge route.
131
+ * The server inspects the current diff, applies deterministic blockers, runs
132
+ * consensus, records a receipt, and direct-merges only the inspected SHA.
133
+ */
134
+ async mergeVerifiedPr(prNumber, automationContext) {
135
+ const res = await req("POST", "/api/v1/admin/pr/merge", { prNumber, automationContext });
136
+ if (res.status === 401) {
137
+ cachedFirebaseToken = null;
138
+ throw new Error("gated merge unauthorized (401)");
139
+ }
140
+ const json = await res.json().catch(() => ({}));
141
+ if (res.ok && json?.ok === true) {
142
+ const result = json?.result && typeof json.result === "object" ? json.result : {};
143
+ const status = result.merged === true || result.status === "merged" ? "merged" : result.status === "auto-merge-enabled" || String(result.action || "").includes("auto-merge") ? "queued" : "accepted";
144
+ return {
145
+ status,
146
+ detail: typeof result.detail === "string" ? result.detail : null,
147
+ actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
148
+ };
149
+ }
150
+ if (res.status === 503 && json?.error === "verify_unavailable") {
151
+ return { status: "retry", reason: json.reason || "verification unavailable" };
152
+ }
153
+ return {
154
+ status: "blocked",
155
+ reason: json?.reason || json?.message || json?.error || `HTTP ${res.status}`,
156
+ actionReceiptId: typeof json?.action_receipt_id === "string" ? json.action_receipt_id : null
157
+ };
158
+ },
159
+ /**
160
+ * Append progress / set terminal status. Returns
161
+ * { task } — applied
162
+ * { terminal: true } — task already terminal (operator cancelled): STOP
163
+ */
164
+ async postProgress(taskId, patch) {
165
+ const res = await req("PATCH", `/api/v1/code-task/${taskId}/progress`, patch);
166
+ if (res.status === 409) return { terminal: true };
167
+ if (res.status === 404) return { terminal: true, missing: true };
168
+ if (!res.ok) throw new Error(`progress failed: HTTP ${res.status}`);
169
+ const json = await res.json();
170
+ return { task: json && json.task };
171
+ },
172
+ /** Read the current task (cancel detection). Null on 404. */
173
+ async getTask(taskId) {
174
+ const res = await req("GET", `/api/v1/code-task/${taskId}`);
175
+ if (res.status === 404) return null;
176
+ if (!res.ok) throw new Error(`getTask failed: HTTP ${res.status}`);
177
+ const json = await res.json();
178
+ return json ? json.task : null;
179
+ },
180
+ /** Fetch bounded, prompt-ready VO knowledge snippets for this task. */
181
+ async getTaskKnowledgeContext(taskId, { query } = {}) {
182
+ const body = {};
183
+ if (typeof query === "string" && query.trim()) body.query = query;
184
+ const res = await req("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`, body);
185
+ if (res.status === 401) {
186
+ cachedFirebaseToken = null;
187
+ throw new Error("knowledge-context unauthorized (401)");
188
+ }
189
+ if (res.status === 404) return null;
190
+ if (!res.ok) throw new Error(`knowledge-context failed: HTTP ${res.status}`);
191
+ return res.json();
192
+ },
193
+ /**
194
+ * Report this machine's rolling-7-day Claude Code token usage (the real
195
+ * weekly-capacity gauge) PLUS the operator's real Claude weekly % (when
196
+ * available). The daemon authenticates as admin, so the target `operatorId`
197
+ * is named explicitly. Best-effort; throws on a non-2xx so the caller can
198
+ * log + move on.
199
+ *
200
+ * `tokens` = { input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens }.
201
+ * Optional: `claudeWeeklyPct` (number) + `claudeWeeklyResetsAt` (ISO string | null).
202
+ */
203
+ async postWeeklyTokens({ operatorId, runnerId: runnerId2, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
204
+ const body = {
205
+ operator_id: operatorId,
206
+ runner_id: runnerId2,
207
+ input_tokens: tokens.input_tokens,
208
+ output_tokens: tokens.output_tokens,
209
+ cache_creation_tokens: tokens.cache_creation_tokens,
210
+ cache_read_tokens: tokens.cache_read_tokens
211
+ };
212
+ if (typeof claudeWeeklyPct === "number") {
213
+ body.claude_weekly_pct = claudeWeeklyPct;
214
+ }
215
+ if (claudeWeeklyResetsAt !== void 0) {
216
+ body.claude_weekly_resets_at = claudeWeeklyResetsAt;
217
+ }
218
+ const res = await req("POST", "/api/v1/weekly-tokens", body);
219
+ if (res.status === 401) {
220
+ cachedFirebaseToken = null;
221
+ throw new Error("weekly-tokens unauthorized (401)");
222
+ }
223
+ if (!res.ok) throw new Error(`weekly-tokens failed: HTTP ${res.status}`);
224
+ return true;
225
+ },
226
+ /**
227
+ * Send a liveness heartbeat (M2). The control-plane upserts it under the
228
+ * authenticated operator so the web shows a TRUE "runner online" signal.
229
+ * Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
230
+ */
231
+ async postHeartbeat({ runnerId: runnerId2, operatorId, uptimeSec, activeTasks, version, servedRepos, servedOperators, availableAgents, accountUsage }) {
232
+ const body = { runner_id: runnerId2 };
233
+ if (operatorId) body.operator_id = operatorId;
234
+ if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
235
+ if (typeof activeTasks === "number") body.active_tasks = activeTasks;
236
+ if (version) body.version = version;
237
+ if (Array.isArray(servedRepos) && servedRepos.length > 0) body.served_repos = servedRepos;
238
+ if (Array.isArray(servedOperators) && servedOperators.length > 0) {
239
+ body.served_operator_ids = servedOperators;
240
+ }
241
+ if (Array.isArray(availableAgents) && availableAgents.length > 0) {
242
+ body.available_agents = availableAgents;
243
+ }
244
+ if (Array.isArray(accountUsage) && accountUsage.length > 0) {
245
+ body.account_usage = accountUsage;
246
+ }
247
+ const res = await req("POST", "/api/v1/runner/heartbeat", body);
248
+ if (res.status === 401) {
249
+ cachedFirebaseToken = null;
250
+ throw new Error("heartbeat unauthorized (401)");
251
+ }
252
+ if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
253
+ return true;
254
+ },
255
+ /** Poll one authenticated runner's durable Mission Control action queue. */
256
+ async pollRunnerControl({ runnerId: runnerId2, operatorId }) {
257
+ const body = { runner_id: runnerId2 };
258
+ if (operatorId) body.operator_id = operatorId;
259
+ const res = await req("POST", "/api/v1/runner/control/poll", body);
260
+ if (res.status === 401) {
261
+ cachedFirebaseToken = null;
262
+ throw new Error("runner control poll unauthorized (401)");
263
+ }
264
+ if (!res.ok) throw new Error(`runner control poll failed: HTTP ${res.status}`);
265
+ const json = await res.json();
266
+ const action = json?.action;
267
+ return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
268
+ },
269
+ /** Acknowledge a maintenance action after the host has restarted the child. */
270
+ async completeRunnerControl(actionId, { runnerId: runnerId2, operatorId, status, detail }) {
271
+ const body = { runner_id: runnerId2, status };
272
+ if (operatorId) body.operator_id = operatorId;
273
+ if (detail) body.detail = detail;
274
+ const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
275
+ if (res.status === 401) {
276
+ cachedFirebaseToken = null;
277
+ throw new Error("runner control completion unauthorized (401)");
278
+ }
279
+ if (!res.ok) throw new Error(`runner control completion failed: HTTP ${res.status}`);
280
+ const json = await res.json();
281
+ return json?.action || null;
282
+ },
283
+ /**
284
+ * Mint a short-lived (~1h), repo-scoped GitHub App installation token for
285
+ * THIS runner's operator (M3). The control-plane keys the mint on the
286
+ * authenticated operator (ctx.operator_id), so the token covers only that
287
+ * operator's installation.
288
+ *
289
+ * Returns { token, expiresAt } on success. In legacy/admin mode it returns
290
+ * null on a miss so the caller may use ambient `gh`. In scoped-operator mode
291
+ * callers pass `{ required: true }`, which fails closed instead of letting a
292
+ * missing/mis-scoped installation fall through to the runner machine's `gh`.
293
+ *
294
+ * The minted token is only usable for push + PR if the GitHub App grants
295
+ * BOTH `Contents: write` (git push) AND `Pull requests: write` (gh pr
296
+ * create) — see docs/vo/github-app-setup-2026-06-18.md. A token missing
297
+ * either scope fails at push (→ ambient fallback) or at `gh pr create`.
298
+ */
299
+ async getInstallationToken({ required = false } = {}) {
300
+ const fail = (reason) => {
301
+ if (required) throw new Error(`installation-token required: ${reason}`);
302
+ return null;
303
+ };
304
+ try {
305
+ const res = await req("POST", "/api/v1/github/installation-token", {});
306
+ if (!res.ok) return fail(`HTTP ${res.status}`);
307
+ const json = await res.json();
308
+ if (!json || !json.token) return fail("missing token");
309
+ return { token: json.token, expiresAt: json.expires_at || null };
310
+ } catch (err) {
311
+ if (required) throw err;
312
+ return null;
313
+ }
314
+ },
315
+ /**
316
+ * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
317
+ * Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'ultracode'),
318
+ * defaulting to 'standard' on any error. Never throws — best-effort.
319
+ */
320
+ async getDispatchMode() {
321
+ try {
322
+ const res = await req("GET", "/api/v1/dispatch-mode-config");
323
+ if (!res.ok) return "standard";
324
+ const json = await res.json();
325
+ return json?.dispatchMode || "standard";
326
+ } catch {
327
+ return "standard";
328
+ }
329
+ }
330
+ };
331
+ }
332
+
333
+ // ../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs
334
+ import { spawnSync } from "node:child_process";
335
+ var DEFAULT_RUNNER_PACKAGE = "@algosuite/vo-mcp@beta";
336
+ var PACKAGE_SPEC_RE = /^@algosuite\/vo-mcp@(beta|latest|\d+(?:\.\d+){0,2}(?:-[\w.-]+)?)$/u;
337
+ function buildMaintenanceCommand(kind, { platform = process.platform, packageSpec = DEFAULT_RUNNER_PACKAGE } = {}) {
338
+ if (!["update", "reinstall"].includes(kind)) return null;
339
+ if (!PACKAGE_SPEC_RE.test(packageSpec)) throw new Error("unsafe runner package spec");
340
+ const command = platform === "win32" ? "npm.cmd" : "npm";
341
+ const args = ["install", "-g", packageSpec];
342
+ if (kind === "reinstall") args.push("--force");
343
+ return { command, args };
344
+ }
345
+ function runHostMaintenance(kind, {
346
+ platform = process.platform,
347
+ packageSpec = DEFAULT_RUNNER_PACKAGE,
348
+ env = process.env,
349
+ spawn: spawn2 = spawnSync,
350
+ log = () => {
351
+ }
352
+ } = {}) {
353
+ if (kind === "reconnect") return { ok: true, status: 0, command: null, args: [] };
354
+ const command = buildMaintenanceCommand(kind, { platform, packageSpec });
355
+ if (!command) return { ok: false, status: 2, command: null, args: [] };
356
+ log(`runner maintenance: ${command.command} ${command.args.join(" ")}`);
357
+ const result = spawn2(command.command, command.args, { stdio: "inherit", env, shell: false });
358
+ const status = typeof result.status === "number" ? result.status : 1;
359
+ return { ok: status === 0, status, command: command.command, args: command.args };
360
+ }
361
+
362
+ // src/cloud/credential-store.ts
363
+ import { homedir } from "node:os";
364
+ import { join, dirname } from "node:path";
365
+ import {
366
+ existsSync,
367
+ mkdirSync,
368
+ readFileSync,
369
+ writeFileSync,
370
+ chmodSync,
371
+ rmSync
372
+ } from "node:fs";
373
+
374
+ // src/cloud/keychain.ts
375
+ import { createRequire } from "node:module";
376
+ var SERVICE = "vo-mcp";
377
+ var ACCOUNT = "refresh-credential";
378
+ var cached;
379
+ function loadKeyring() {
380
+ if (cached !== void 0) return cached;
381
+ try {
382
+ const req = createRequire(import.meta.url);
383
+ const mod = req("@napi-rs/keyring");
384
+ cached = mod && typeof mod.Entry === "function" ? mod : null;
385
+ } catch {
386
+ cached = null;
387
+ }
388
+ return cached;
389
+ }
390
+ function keychainAvailable() {
391
+ return loadKeyring() !== null;
392
+ }
393
+ function keychainGet() {
394
+ const k = loadKeyring();
395
+ if (!k) return null;
396
+ try {
397
+ return new k.Entry(SERVICE, ACCOUNT).getPassword();
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ function keychainSet(secret) {
403
+ const k = loadKeyring();
404
+ if (!k) return false;
405
+ try {
406
+ new k.Entry(SERVICE, ACCOUNT).setPassword(secret);
407
+ return true;
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+ function keychainDelete() {
413
+ const k = loadKeyring();
414
+ if (!k) return false;
415
+ try {
416
+ return new k.Entry(SERVICE, ACCOUNT).deletePassword();
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+
422
+ // src/cloud/credential-store.ts
423
+ var realKeychain = {
424
+ available: keychainAvailable,
425
+ get: keychainGet,
426
+ set: keychainSet,
427
+ delete: keychainDelete
428
+ };
429
+ function credentialPath(env = process.env) {
430
+ const override = env["VO_MCP_CREDENTIALS_PATH"]?.trim();
431
+ if (override) return override;
432
+ return join(homedir(), ".config", "vo-mcp", "credentials.json");
433
+ }
434
+ function keychainEnabled(env, keychain) {
435
+ const disabled = (env["VO_MCP_DISABLE_KEYCHAIN"] ?? "").trim().toLowerCase();
436
+ if (disabled === "1" || disabled === "true" || disabled === "yes") return false;
437
+ return keychain.available();
438
+ }
439
+ function deserialize(raw) {
440
+ try {
441
+ const parsed = JSON.parse(raw);
442
+ const refresh = typeof parsed.refresh_token === "string" ? parsed.refresh_token.trim() : "";
443
+ const apiKey = typeof parsed.api_key === "string" ? parsed.api_key.trim() : "";
444
+ const voCred = typeof parsed.vo_credential === "string" ? parsed.vo_credential.trim() : "";
445
+ if (!voCred && (!refresh || !apiKey)) return null;
446
+ return {
447
+ ...refresh ? { refresh_token: refresh } : {},
448
+ ...apiKey ? { api_key: apiKey } : {},
449
+ ...voCred ? { vo_credential: voCred } : {},
450
+ ...typeof parsed.vo_credential_expires_at === "string" ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {},
451
+ ...typeof parsed.email === "string" ? { email: parsed.email } : {},
452
+ ...typeof parsed.stored_at === "string" ? { stored_at: parsed.stored_at } : {}
453
+ };
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+ function readFromFile(env) {
459
+ try {
460
+ const p = credentialPath(env);
461
+ if (!existsSync(p)) return null;
462
+ return deserialize(readFileSync(p, "utf8"));
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+ function readStoredCredential(env = process.env, keychain = realKeychain) {
468
+ if (keychainEnabled(env, keychain)) {
469
+ const raw = keychain.get();
470
+ const fromKeychain = raw ? deserialize(raw) : null;
471
+ if (fromKeychain) return fromKeychain;
472
+ }
473
+ return readFromFile(env);
474
+ }
475
+
476
+ // src/runner/supervisor-child-env.mjs
477
+ import { hostname as systemHostname } from "node:os";
478
+
479
+ // src/runner-readiness.mjs
480
+ function failed({ paired = false, operatorId = null, tenantId = null, githubReady = null, error, message }) {
481
+ return { ok: false, paired, operatorId, tenantId, githubReady, error, message };
482
+ }
483
+ async function responseBody(response) {
484
+ try {
485
+ const value = await response.json();
486
+ return value && typeof value === "object" ? value : {};
487
+ } catch {
488
+ return {};
489
+ }
490
+ }
491
+ function serverMessage(body, fallback) {
492
+ return typeof body.message === "string" && body.message.trim() ? body.message.trim() : fallback;
493
+ }
494
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
495
+ const controller = new AbortController();
496
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
497
+ try {
498
+ return await fetchImpl(url, { ...init, signal: controller.signal });
499
+ } finally {
500
+ clearTimeout(timer);
501
+ }
502
+ }
503
+ async function probeRunnerReadiness({
504
+ controlPlaneUrl,
505
+ token,
506
+ fetchImpl = fetch,
507
+ requireGithub = false,
508
+ timeoutMs = 1e4
509
+ }) {
510
+ const base = controlPlaneUrl.replace(/\/+$/u, "");
511
+ const headers = { authorization: `Bearer ${token}` };
512
+ let identityResponse;
513
+ try {
514
+ identityResponse = await fetchWithTimeout(
515
+ fetchImpl,
516
+ `${base}/api/v1/auth/me`,
517
+ { headers },
518
+ timeoutMs
519
+ );
520
+ } catch (error) {
521
+ const detail = error instanceof Error ? error.message : String(error);
522
+ return failed({
523
+ error: "control_plane_unreachable",
524
+ message: `Virtual Office could not be reached: ${detail}`
525
+ });
526
+ }
527
+ const identity = await responseBody(identityResponse);
528
+ if (!identityResponse.ok) {
529
+ return failed({
530
+ error: "credential_rejected",
531
+ message: "The saved pairing is expired or revoked. Pair this computer again."
532
+ });
533
+ }
534
+ 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()) {
535
+ return failed({
536
+ error: "operator_identity_missing",
537
+ message: "The pairing credential is valid but has no operator identity. Pair this computer again."
538
+ });
539
+ }
540
+ const operatorId = identity.operator_id.trim();
541
+ const tenantId = identity.tenant_id.trim();
542
+ if (!requireGithub) {
543
+ return {
544
+ ok: true,
545
+ paired: true,
546
+ operatorId,
547
+ tenantId,
548
+ githubReady: null,
549
+ error: null,
550
+ message: "Paired to Virtual Office."
551
+ };
552
+ }
553
+ let githubResponse;
554
+ try {
555
+ githubResponse = await fetchWithTimeout(
556
+ fetchImpl,
557
+ `${base}/api/v1/github/installation-token`,
558
+ {
559
+ method: "POST",
560
+ headers: { ...headers, "content-type": "application/json" },
561
+ body: "{}"
562
+ },
563
+ timeoutMs
564
+ );
565
+ } catch (error) {
566
+ const detail = error instanceof Error ? error.message : String(error);
567
+ return failed({
568
+ paired: true,
569
+ operatorId,
570
+ tenantId,
571
+ githubReady: false,
572
+ error: "github_preflight_unreachable",
573
+ message: `GitHub publication readiness could not be checked: ${detail}`
574
+ });
575
+ }
576
+ const github = await responseBody(githubResponse);
577
+ if (!githubResponse.ok || typeof github.token !== "string" || !github.token) {
578
+ const error = typeof github.error === "string" && github.error ? github.error : "github_not_ready";
579
+ return failed({
580
+ paired: true,
581
+ operatorId,
582
+ tenantId,
583
+ githubReady: false,
584
+ error,
585
+ message: serverMessage(github, `GitHub publication preflight failed (HTTP ${githubResponse.status}).`)
586
+ });
587
+ }
588
+ return {
589
+ ok: true,
590
+ paired: true,
591
+ operatorId,
592
+ tenantId,
593
+ githubReady: true,
594
+ error: null,
595
+ message: "Paired and ready to publish through the Algosuite GitHub App."
596
+ };
597
+ }
598
+ function pairedOperatorScope(readiness) {
599
+ return readiness?.ok === true && readiness?.paired === true && typeof readiness.operatorId === "string" && readiness.operatorId.trim() ? readiness.operatorId.trim() : null;
600
+ }
601
+
602
+ // src/runner/supervisor-child-env.mjs
603
+ function resolveSupervisorRunnerId(env = {}, hostname = systemHostname) {
604
+ const explicit = String(env.VO_CODE_RUNNER_ID || "").trim();
605
+ return explicit || `vo-code-runner-${hostname()}`;
606
+ }
607
+ function buildSupervisorChildEnv({
608
+ baseEnv = {},
609
+ controlPlaneUrl,
610
+ explicitAdminToken = null,
611
+ pairedOperatorId = null
612
+ } = {}) {
613
+ const childEnv = {
614
+ ...baseEnv,
615
+ VO_CONTROL_PLANE_URL: controlPlaneUrl
616
+ };
617
+ const adminToken = typeof explicitAdminToken === "string" ? explicitAdminToken.trim() : "";
618
+ const operatorId = typeof pairedOperatorId === "string" ? pairedOperatorId.trim() : "";
619
+ if (adminToken) childEnv.VO_CONTROL_PLANE_ADMIN_TOKEN = adminToken;
620
+ else delete childEnv.VO_CONTROL_PLANE_ADMIN_TOKEN;
621
+ if (operatorId) childEnv.VO_CODE_RUNNER_OPERATOR_IDS = operatorId;
622
+ return childEnv;
623
+ }
624
+ async function prepareSupervisorAuth({
625
+ baseEnv = {},
626
+ storedCredential = null,
627
+ controlPlaneUrl,
628
+ probeReadiness = probeRunnerReadiness
629
+ } = {}) {
630
+ const explicitAdminToken = baseEnv.VO_CONTROL_PLANE_ADMIN_TOKEN?.trim();
631
+ const token = explicitAdminToken || storedCredential?.vo_credential;
632
+ if (!token) throw new Error("runner is not paired; run `vo-mcp pair` once on this host");
633
+ let operatorId = String(baseEnv.VO_CODE_RUNNER_OPERATOR_IDS || "").split(/[\s,]+/u).filter(Boolean)[0] || void 0;
634
+ if (!explicitAdminToken) {
635
+ const readiness = await probeReadiness({ controlPlaneUrl, token, requireGithub: true });
636
+ if (!readiness.ok) throw new Error(`runner readiness failed: ${readiness.message}`);
637
+ operatorId = pairedOperatorScope(readiness) || void 0;
638
+ if (!operatorId) throw new Error("runner readiness failed: paired operator scope is missing");
639
+ }
640
+ const childEnv = buildSupervisorChildEnv({
641
+ baseEnv,
642
+ controlPlaneUrl,
643
+ explicitAdminToken,
644
+ pairedOperatorId: explicitAdminToken ? null : operatorId
645
+ });
646
+ const clientEnv = {
647
+ ...baseEnv,
648
+ VO_CONTROL_PLANE_ADMIN_TOKEN: token,
649
+ VO_CONTROL_PLANE_URL: controlPlaneUrl,
650
+ ...operatorId ? { VO_CODE_RUNNER_OPERATOR_IDS: operatorId } : {}
651
+ };
652
+ return { childEnv, clientEnv, operatorId };
653
+ }
654
+
655
+ // src/runner-supervisor.mjs
656
+ var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
657
+ var POLL_MS = 5e3;
658
+ var CHILD_START_MS = 1500;
659
+ var childEntry = join2(dirname2(fileURLToPath(import.meta.url)), "runner-cli.js");
660
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
661
+ var runnerId = resolveSupervisorRunnerId(process.env);
662
+ function packageVersion() {
663
+ try {
664
+ return createRequire2(import.meta.url)("../package.json").version || "unknown";
665
+ } catch {
666
+ return "unknown";
667
+ }
668
+ }
669
+ function spawnChild(childEnv) {
670
+ return spawn(process.execPath, [childEntry], {
671
+ env: childEnv,
672
+ stdio: "inherit",
673
+ windowsHide: true
674
+ });
675
+ }
676
+ async function localStatus() {
677
+ try {
678
+ const port = Number(process.env.VO_CODE_RUNNER_CONTROL_PORT || 7787);
679
+ const response = await fetch(`http://127.0.0.1:${port}/status`, { signal: AbortSignal.timeout(800) });
680
+ if (!response.ok) return null;
681
+ const body = await response.json();
682
+ return body?.ok === true ? body : null;
683
+ } catch {
684
+ return null;
685
+ }
686
+ }
687
+ async function waitForLocalRunner(child) {
688
+ const deadline = Date.now() + 15e3;
689
+ while (Date.now() < deadline) {
690
+ if (child.exitCode !== null) return false;
691
+ if ((await localStatus())?.running === true) return true;
692
+ await sleep(500);
693
+ }
694
+ return false;
695
+ }
696
+ async function stopChild(child) {
697
+ if (!child || child.exitCode !== null) return;
698
+ child.kill(process.platform === "win32" ? void 0 : "SIGTERM");
699
+ await Promise.race([
700
+ new Promise((resolve) => child.once("exit", resolve)),
701
+ sleep(15e3)
702
+ ]);
703
+ if (child.exitCode === null) child.kill("SIGKILL");
704
+ }
705
+ async function main() {
706
+ const stored = readStoredCredential();
707
+ const controlPlaneUrl = process.env.VO_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL;
708
+ const { childEnv, clientEnv, operatorId } = await prepareSupervisorAuth({
709
+ baseEnv: process.env,
710
+ storedCredential: stored,
711
+ controlPlaneUrl
712
+ });
713
+ const client = createControlPlaneClient({ baseUrl: controlPlaneUrl, env: clientEnv });
714
+ let child = spawnChild(childEnv);
715
+ let stopping = false;
716
+ let handling = false;
717
+ const respawn = () => {
718
+ if (!stopping && !handling && (!child || child.exitCode !== null)) child = spawnChild(childEnv);
719
+ };
720
+ child.on("exit", () => setTimeout(respawn, 2e3));
721
+ const shutdown = async () => {
722
+ if (stopping) return;
723
+ stopping = true;
724
+ await stopChild(child);
725
+ };
726
+ process.on("SIGINT", () => {
727
+ void shutdown().finally(() => process.exit(0));
728
+ });
729
+ process.on("SIGTERM", () => {
730
+ void shutdown().finally(() => process.exit(0));
731
+ });
732
+ while (!stopping) {
733
+ try {
734
+ const action = await client.pollRunnerControl({ runnerId, ...operatorId ? { operatorId } : {} });
735
+ if (!action) {
736
+ await sleep(POLL_MS);
737
+ continue;
738
+ }
739
+ handling = true;
740
+ const beforeStop = await localStatus();
741
+ if (beforeStop && Number(beforeStop.activeTasks || 0) > 0) {
742
+ await client.completeRunnerControl(action.actionId, {
743
+ runnerId,
744
+ ...operatorId ? { operatorId } : {},
745
+ status: "failed",
746
+ detail: `deferred safely: ${beforeStop.activeTasks} active task(s); retry when the runner is idle`
747
+ });
748
+ handling = false;
749
+ continue;
750
+ }
751
+ await stopChild(child);
752
+ const result = runHostMaintenance(action.kind, {
753
+ env: clientEnv,
754
+ log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
755
+ });
756
+ child = spawnChild(childEnv);
757
+ await sleep(CHILD_START_MS);
758
+ if (child.exitCode !== null || !await waitForLocalRunner(child)) result.ok = false;
759
+ await client.completeRunnerControl(action.actionId, {
760
+ runnerId,
761
+ ...operatorId ? { operatorId } : {},
762
+ status: result.ok ? "succeeded" : "failed",
763
+ detail: result.ok ? `runner ${packageVersion()} reconnected` : `maintenance exited ${result.status}`
764
+ });
765
+ handling = false;
766
+ } catch (error) {
767
+ console.error(`[vo-runner supervisor] ${error instanceof Error ? error.message : String(error)}`);
768
+ handling = false;
769
+ await sleep(POLL_MS);
770
+ }
771
+ }
772
+ }
773
+ main().catch((error) => {
774
+ console.error(`[vo-runner supervisor] fatal: ${error instanceof Error ? error.message : String(error)}`);
775
+ process.exitCode = 1;
776
+ });
777
+ //# sourceMappingURL=runner-supervisor.js.map