@algosuite/vo-mcp 0.2.0-beta.21 → 0.2.0-beta.23

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.
@@ -2241,6 +2241,37 @@ var init_spend_cap_shim = __esm({
2241
2241
  }
2242
2242
  });
2243
2243
 
2244
+ // ../../scripts/virtual-office/code-runner/installation-token.mjs
2245
+ async function fetchInstallationToken({ req, required = false, readOnly = false, repo = null }) {
2246
+ const fail = (reason) => {
2247
+ if (required) throw new Error(`installation-token required: ${reason}`);
2248
+ return null;
2249
+ };
2250
+ try {
2251
+ const res = await req(
2252
+ "POST",
2253
+ "/api/v1/github/installation-token",
2254
+ readOnly ? { scope: "read", ...repo ? { repo } : {} } : {},
2255
+ readOnly ? { timeoutMs: READ_TOKEN_TIMEOUT_MS } : {}
2256
+ );
2257
+ if (!res.ok) return fail(`HTTP ${res.status}`);
2258
+ const json = await res.json();
2259
+ if (!json || !json.token) return fail("missing token");
2260
+ if (readOnly && json.scope !== "read") return fail("control plane did not confirm a read-only grant");
2261
+ return { token: json.token, expiresAt: json.expires_at || null };
2262
+ } catch (err) {
2263
+ if (required) throw err;
2264
+ return null;
2265
+ }
2266
+ }
2267
+ var READ_TOKEN_TIMEOUT_MS;
2268
+ var init_installation_token = __esm({
2269
+ "../../scripts/virtual-office/code-runner/installation-token.mjs"() {
2270
+ "use strict";
2271
+ READ_TOKEN_TIMEOUT_MS = 15e3;
2272
+ }
2273
+ });
2274
+
2244
2275
  // src/runner/control-plane-auth-stub.mjs
2245
2276
  var control_plane_auth_stub_exports = {};
2246
2277
  __export(control_plane_auth_stub_exports, {
@@ -2564,37 +2595,9 @@ function createControlPlaneClient({
2564
2595
  const json = await res.json();
2565
2596
  return json?.action || null;
2566
2597
  },
2567
- /**
2568
- * Mint a short-lived (~1h), repo-scoped GitHub App installation token for
2569
- * THIS runner's operator (M3). The control-plane keys the mint on the
2570
- * authenticated operator (ctx.operator_id), so the token covers only that
2571
- * operator's installation.
2572
- *
2573
- * Returns { token, expiresAt } on success. In legacy/admin mode it returns
2574
- * null on a miss so the caller may use ambient `gh`. In scoped-operator mode
2575
- * callers pass `{ required: true }`, which fails closed instead of letting a
2576
- * missing/mis-scoped installation fall through to the runner machine's `gh`.
2577
- *
2578
- * The minted token is only usable for push + PR if the GitHub App grants
2579
- * BOTH `Contents: write` (git push) AND `Pull requests: write` (gh pr
2580
- * create) — see docs/vo/github-app-setup-2026-06-18.md. A token missing
2581
- * either scope fails at push (→ ambient fallback) or at `gh pr create`.
2582
- */
2583
- async getInstallationToken({ required = false } = {}) {
2584
- const fail = (reason) => {
2585
- if (required) throw new Error(`installation-token required: ${reason}`);
2586
- return null;
2587
- };
2588
- try {
2589
- const res = await req("POST", "/api/v1/github/installation-token", {});
2590
- if (!res.ok) return fail(`HTTP ${res.status}`);
2591
- const json = await res.json();
2592
- if (!json || !json.token) return fail("missing token");
2593
- return { token: json.token, expiresAt: json.expires_at || null };
2594
- } catch (err) {
2595
- if (required) throw err;
2596
- return null;
2597
- }
2598
+ /** Mint a GitHub App installation token — see installation-token.mjs. */
2599
+ async getInstallationToken({ required = false, readOnly = false, repo = null } = {}) {
2600
+ return fetchInstallationToken({ req, required, readOnly, repo });
2598
2601
  },
2599
2602
  /**
2600
2603
  * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
@@ -2617,6 +2620,7 @@ var cachedFirebaseToken;
2617
2620
  var init_control_plane_client = __esm({
2618
2621
  "../../scripts/virtual-office/code-runner/control-plane-client.mjs"() {
2619
2622
  "use strict";
2623
+ init_installation_token();
2620
2624
  cachedFirebaseToken = null;
2621
2625
  }
2622
2626
  });
@@ -3844,6 +3848,11 @@ function itemText(item) {
3844
3848
  }
3845
3849
  return "";
3846
3850
  }
3851
+ function parseCodexVersion(stdout) {
3852
+ if (typeof stdout !== "string") return null;
3853
+ const match = stdout.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u);
3854
+ return match ? match[1] : null;
3855
+ }
3847
3856
  function parseCodexEvent(line) {
3848
3857
  const trimmed = String(line || "").trim();
3849
3858
  if (!trimmed) return null;
@@ -3938,7 +3947,7 @@ var init_codex_runner = __esm({
3938
3947
  ...this.getSpawnOptions({ bin }),
3939
3948
  windowsHide: true,
3940
3949
  timeout: 3e3,
3941
- stdio: "ignore"
3950
+ encoding: "utf8"
3942
3951
  });
3943
3952
  if (version.error) {
3944
3953
  return { installed: false, authenticated: false, message: `codex not found on PATH: ${version.error.message}` };
@@ -3946,6 +3955,8 @@ var init_codex_runner = __esm({
3946
3955
  if (version.status !== 0) {
3947
3956
  return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
3948
3957
  }
3958
+ const cliVersion = parseCodexVersion(version.stdout);
3959
+ const versionField = cliVersion ? { version: cliVersion } : {};
3949
3960
  const login = this.spawn(bin, ["login", "status"], {
3950
3961
  ...this.getSpawnOptions({ bin }),
3951
3962
  windowsHide: true,
@@ -3960,18 +3971,21 @@ ${login.stderr || ""}`.trim();
3960
3971
  return {
3961
3972
  installed: true,
3962
3973
  authenticated: true,
3974
+ ...versionField,
3963
3975
  message: "codex API key available (no persisted ChatGPT login)"
3964
3976
  };
3965
3977
  }
3966
3978
  return {
3967
3979
  installed: true,
3968
3980
  authenticated: false,
3981
+ ...versionField,
3969
3982
  message: output || login.error?.message || "codex is installed but not logged in"
3970
3983
  };
3971
3984
  }
3972
3985
  return {
3973
3986
  installed: true,
3974
3987
  authenticated: true,
3988
+ ...versionField,
3975
3989
  message: output || "codex login status succeeded"
3976
3990
  };
3977
3991
  } catch (err) {
@@ -6417,12 +6431,23 @@ function resolveAgentClaimContext(provider, defaultAgent) {
6417
6431
  }
6418
6432
  async function collectAgentAvailability({
6419
6433
  agents = listAgents(),
6420
- runnerFor = (agent) => resolveRunner({ VO_CODE_RUNNER_AGENT: agent }).runner
6434
+ runnerFor = (agent) => resolveRunner({ VO_CODE_RUNNER_AGENT: agent }).runner,
6435
+ probeTimeoutMs = PROBE_TIMEOUT_MS
6421
6436
  } = {}) {
6422
6437
  const probes = agents.map(async (agent) => {
6438
+ const degraded = { agent, installed: false, authenticated: false };
6423
6439
  try {
6424
- const r = await runnerFor(agent).checkAuth();
6425
- return { agent, installed: Boolean(r?.installed), authenticated: Boolean(r?.authenticated) };
6440
+ const r = await Promise.race([
6441
+ Promise.resolve().then(() => runnerFor(agent).checkAuth()),
6442
+ new Promise((resolve2) => setTimeout(() => resolve2(null), probeTimeoutMs))
6443
+ ]);
6444
+ if (!r) return degraded;
6445
+ return {
6446
+ agent,
6447
+ installed: Boolean(r?.installed),
6448
+ authenticated: Boolean(r?.authenticated),
6449
+ ...typeof r?.version === "string" && r.version ? { version: r.version } : {}
6450
+ };
6426
6451
  } catch {
6427
6452
  return { agent, installed: false, authenticated: false };
6428
6453
  }
@@ -6438,28 +6463,53 @@ function makeAgentAvailabilityProvider({
6438
6463
  } = {}) {
6439
6464
  let cached2 = null;
6440
6465
  let fetchedAt = 0;
6441
- let inFlight = false;
6466
+ let inFlight = null;
6467
+ const refresh = () => {
6468
+ if (inFlight) return inFlight;
6469
+ try {
6470
+ inFlight = Promise.resolve(collect()).then((list) => {
6471
+ cached2 = list;
6472
+ fetchedAt = now();
6473
+ }).catch((e) => onError(e)).finally(() => {
6474
+ inFlight = null;
6475
+ });
6476
+ } catch (e) {
6477
+ inFlight = null;
6478
+ onError(e);
6479
+ return Promise.resolve();
6480
+ }
6481
+ return inFlight;
6482
+ };
6442
6483
  return {
6443
6484
  get() {
6444
- if (!inFlight && now() - fetchedAt >= ttlMs) {
6445
- inFlight = true;
6446
- Promise.resolve(collect()).then((list) => {
6447
- cached2 = list;
6448
- fetchedAt = now();
6449
- }).catch((e) => onError(e)).finally(() => {
6450
- inFlight = false;
6451
- });
6452
- }
6485
+ if (!inFlight && now() - fetchedAt >= ttlMs) refresh();
6486
+ return cached2;
6487
+ },
6488
+ /**
6489
+ * Resolve once a probe has actually completed, so the FIRST heartbeat can
6490
+ * report real agents. Without this the daemon heartbeats immediately with
6491
+ * `available_agents: []` (the cache is null until the first probe lands),
6492
+ * and a supervisor activation attestation — which requires the default
6493
+ * agent present, installed AND authenticated — can never be satisfied. The
6494
+ * supervisor then kills and restarts the child forever: observed on
6495
+ * JacksPC 2026-07-25, uptime stuck at 1s across every heartbeat.
6496
+ *
6497
+ * Bounded on purpose: a wedged probe must delay startup, never prevent it.
6498
+ */
6499
+ async ready(timeoutMs = 2e4) {
6500
+ if (Array.isArray(cached2)) return cached2;
6501
+ await Promise.race([refresh(), new Promise((r) => setTimeout(r, timeoutMs))]);
6453
6502
  return cached2;
6454
6503
  }
6455
6504
  };
6456
6505
  }
6457
- var DEFAULT_TTL_MS;
6506
+ var DEFAULT_TTL_MS, PROBE_TIMEOUT_MS;
6458
6507
  var init_agent_availability = __esm({
6459
6508
  "../../scripts/virtual-office/code-runner/agent-availability.mjs"() {
6460
6509
  "use strict";
6461
6510
  init_resolve_runner();
6462
6511
  DEFAULT_TTL_MS = 5 * 60 * 1e3;
6512
+ PROBE_TIMEOUT_MS = 1e4;
6463
6513
  }
6464
6514
  });
6465
6515
 
@@ -8823,11 +8873,41 @@ var init_reconnect_backoff = __esm({
8823
8873
  }
8824
8874
  });
8825
8875
 
8876
+ // ../../scripts/virtual-office/code-runner/redact-tokens.mjs
8877
+ function redactSecrets(text) {
8878
+ if (typeof text !== "string" || text.length === 0) return text;
8879
+ let out = text;
8880
+ for (const [re, mask] of TOKEN_PATTERNS) out = out.replace(re, mask);
8881
+ return out;
8882
+ }
8883
+ function redactPatch(patch) {
8884
+ if (!patch || typeof patch !== "object") return patch;
8885
+ const out = Array.isArray(patch) ? [...patch] : { ...patch };
8886
+ for (const [k, v] of Object.entries(out)) {
8887
+ if (typeof v === "string") out[k] = redactSecrets(v);
8888
+ else if (v && typeof v === "object") out[k] = redactPatch(v);
8889
+ }
8890
+ return out;
8891
+ }
8892
+ var TOKEN_PATTERNS;
8893
+ var init_redact_tokens = __esm({
8894
+ "../../scripts/virtual-office/code-runner/redact-tokens.mjs"() {
8895
+ "use strict";
8896
+ TOKEN_PATTERNS = [
8897
+ [/\b(?:gh[oprsu]|vocred|npm)_[A-Za-z0-9._-]{10,}\b/gu, "[REDACTED]"],
8898
+ [/\bgithub_pat_[A-Za-z0-9_]{10,}\b/gu, "[REDACTED]"],
8899
+ // Keep the scheme so the line still reads as an auth header, matching
8900
+ // sanitizeMaintenanceDiagnostic's behaviour.
8901
+ [/\bBearer\s+[A-Za-z0-9._~+/-]{10,}=*/giu, "Bearer [REDACTED]"]
8902
+ ];
8903
+ }
8904
+ });
8905
+
8826
8906
  // ../../scripts/virtual-office/code-runner/task-helpers.mjs
8827
8907
  function makeSafeProgress(log3) {
8828
8908
  return async (client, id, patch) => {
8829
8909
  try {
8830
- const r = await client.postProgress(id, patch);
8910
+ const r = await client.postProgress(id, redactPatch(patch));
8831
8911
  if (r && r.terminal) log3(`task ${id} is terminal server-side; stopping updates`);
8832
8912
  return r;
8833
8913
  } catch (err) {
@@ -8853,20 +8933,34 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
8853
8933
  "### Prompt",
8854
8934
  "",
8855
8935
  "```",
8856
- String(task.prompt).slice(0, 2e3),
8936
+ redactSecrets(String(task.prompt)).slice(0, 2e3),
8857
8937
  "```",
8858
8938
  "",
8859
8939
  "### Agent summary",
8860
8940
  "",
8861
- String(run.summary || "").slice(0, 2e3),
8941
+ redactSecrets(String(run.summary || "")).slice(0, 2e3),
8862
8942
  "",
8863
8943
  "---",
8864
8944
  armAutoMerge ? "_Opened by the AlgoHQ code-runner daemon. After CI passes, the watcher must obtain a durable consensus receipt and merge the exact verified SHA._" : "_Opened by the AlgoHQ code-runner daemon. This PR awaits the verify-before-act gate / operator review \u2014 it is NOT auto-merged._"
8865
8945
  ].filter((l) => l !== "").join("\n");
8866
8946
  }
8947
+ async function mintRunnerGithubTokens({ client, taskId, log: log3, repo = null, requirePublish = false }) {
8948
+ const publishToken = (await client.getInstallationToken({ required: requirePublish }))?.token ?? null;
8949
+ let agentReadToken = null;
8950
+ let reason = null;
8951
+ try {
8952
+ agentReadToken = (await client.getInstallationToken({ readOnly: true, repo }))?.token ?? null;
8953
+ if (!agentReadToken) reason = "control plane returned no confirmed read-only grant";
8954
+ } catch (err) {
8955
+ reason = err?.message ?? String(err);
8956
+ }
8957
+ if (!agentReadToken) log3(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
8958
+ return { publishToken, agentReadToken };
8959
+ }
8867
8960
  var init_task_helpers = __esm({
8868
8961
  "../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
8869
8962
  "use strict";
8963
+ init_redact_tokens();
8870
8964
  }
8871
8965
  });
8872
8966
 
@@ -8882,8 +8976,12 @@ function safeBaseEnv(env2 = {}) {
8882
8976
  }
8883
8977
  return result;
8884
8978
  }
8885
- function buildAgentProcessEnv(env2, { agent = "agent", runnerId = "vo-runner", taskId = "task" } = {}) {
8979
+ function buildAgentProcessEnv(env2, { agent = "agent", runnerId = "vo-runner", taskId = "task", githubReadToken = null } = {}) {
8886
8980
  const base = safeBaseEnv(env2);
8981
+ if (typeof githubReadToken === "string" && githubReadToken) {
8982
+ base.GH_TOKEN = githubReadToken;
8983
+ base.GITHUB_TOKEN = githubReadToken;
8984
+ }
8887
8985
  if (String(env2?.AGENT_ID || "").trim()) return { ...base, AGENT_ID: env2.AGENT_ID };
8888
8986
  const generated = [
8889
8987
  "vo",
@@ -9574,7 +9672,7 @@ async function processOneTask(client, task, cfg) {
9574
9672
  const wt = await Promise.resolve(createFixWorktree("code-task", { source: id.slice(0, 8), repo: task.repo }));
9575
9673
  worktreeName = wt.worktreeName;
9576
9674
  if (!worktreeName || !wt.worktreeDir) throw new Error("worktree isolation failure \u2014 refusing to run in the main tree");
9577
- const githubToken = (await client.getInstallationToken({ required: cfg.requireGithubAppAuth }))?.token ?? null;
9675
+ const { publishToken: githubToken, agentReadToken: agentGithubReadToken } = await mintRunnerGithubTokens({ client, taskId: id, log: log2, repo: task.repo, requirePublish: cfg.requireGithubAppAuth });
9578
9676
  const parentTask = task.resumed_from ? await client.getTask(task.resumed_from).catch(() => null) : null;
9579
9677
  const continuationRestore = await prepareContinuationBranch(wt.worktreeDir, {
9580
9678
  task,
@@ -9609,7 +9707,7 @@ async function processOneTask(client, task, cfg) {
9609
9707
  model,
9610
9708
  effort: effectiveEffort,
9611
9709
  maxBudgetUsd: effectiveMaxBudgetUsd,
9612
- env: buildAgentProcessEnv(process.env, { agent: cfg.agent, runnerId: cfg.runnerId, taskId: id }),
9710
+ env: buildAgentProcessEnv(process.env, { agent: cfg.agent, runnerId: cfg.runnerId, taskId: id, githubReadToken: agentGithubReadToken }),
9613
9711
  onProgress: (text) => {
9614
9712
  void safeProgress(client, id, runnerStagePatch("agent_working", text));
9615
9713
  },
@@ -9794,6 +9892,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
9794
9892
  const runWatch = makeWatchRunner({ client, log: log2, maxFixAttempts: cfg.watchMaxFix, autoMergeEnabled: cfg.armAutoMerge });
9795
9893
  const watchCoordinator = makeWatchCycleCoordinator({ runWatch, log: log2, intervalMs: cfg.watchIntervalSec * 1e3 });
9796
9894
  const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log2(`agent probe failed: ${e.message}`) });
9895
+ await agentAvailability.ready();
9797
9896
  const accountUsage = makeAccountUsageProvider();
9798
9897
  const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, capacityController, localModelController: createLocalModelRemoteController({ env: env2, log: log2 }), getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
9799
9898
  const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log: log2 });