@algosuite/vo-mcp 0.2.0-beta.71 → 0.2.0-beta.73

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.
@@ -314,21 +314,21 @@ function backupConfigOnce(configPath) {
314
314
  copyFileSync(configPath, backupPath);
315
315
  return backupPath;
316
316
  }
317
- function writeFileAtomic(path23, content) {
318
- sweepStaleTempFiles(path23);
319
- const temp = `${path23}.vo-mcp-tmp-${process.pid}-${Date.now()}`;
317
+ function writeFileAtomic(path24, content) {
318
+ sweepStaleTempFiles(path24);
319
+ const temp = `${path24}.vo-mcp-tmp-${process.pid}-${Date.now()}`;
320
320
  try {
321
321
  writeFileSync2(temp, content, { encoding: "utf8", mode: 384 });
322
- if (existsSync3(path23)) {
322
+ if (existsSync3(path24)) {
323
323
  try {
324
- chmodSync2(temp, statSync(path23).mode & 511);
324
+ chmodSync2(temp, statSync(path24).mode & 511);
325
325
  } catch {
326
326
  }
327
327
  }
328
328
  let lastErr = null;
329
329
  for (let attempt = 0; attempt < RENAME_RETRIES; attempt += 1) {
330
330
  try {
331
- renameSync(temp, path23);
331
+ renameSync(temp, path24);
332
332
  return;
333
333
  } catch (err) {
334
334
  lastErr = err;
@@ -337,7 +337,7 @@ function writeFileAtomic(path23, content) {
337
337
  sleepSync(RENAME_RETRY_MS);
338
338
  }
339
339
  }
340
- writeFileSync2(path23, content, "utf8");
340
+ writeFileSync2(path24, content, "utf8");
341
341
  try {
342
342
  unlinkSync2(temp);
343
343
  } catch {
@@ -503,8 +503,8 @@ function tablePath(line) {
503
503
  function tableSections(lines) {
504
504
  const starts = [];
505
505
  for (let index = 0; index < lines.length; index += 1) {
506
- const path23 = tablePath(lines[index] ?? "");
507
- if (path23) starts.push({ path: path23, start: index });
506
+ const path24 = tablePath(lines[index] ?? "");
507
+ if (path24) starts.push({ path: path24, start: index });
508
508
  }
509
509
  return starts.map((section, index) => ({
510
510
  ...section,
@@ -696,11 +696,11 @@ function resolveLinuxConfigHome(home, env2) {
696
696
  const configured = env2["XDG_CONFIG_HOME"]?.trim();
697
697
  return configured && isAbsolute2(configured) ? configured : join5(home, ".config");
698
698
  }
699
- function launcherIsCurrent(path23, desiredContent, label, log2) {
700
- if (!existsSync6(path23)) return false;
701
- if (readFileSync5(path23, "utf8") === desiredContent) return true;
702
- const backupPath = `${path23}.backup-${Date.now()}`;
703
- copyFileSync2(path23, backupPath);
699
+ function launcherIsCurrent(path24, desiredContent, label, log2) {
700
+ if (!existsSync6(path24)) return false;
701
+ if (readFileSync5(path24, "utf8") === desiredContent) return true;
702
+ const backupPath = `${path24}.backup-${Date.now()}`;
703
+ copyFileSync2(path24, backupPath);
704
704
  log2(` Backed up existing ${label} to: ${backupPath}`);
705
705
  return false;
706
706
  }
@@ -916,17 +916,17 @@ function resolveDesktopConfigPath(home, plat, appData) {
916
916
  }
917
917
  return join6(home, ".config", "Claude", "claude_desktop_config.json");
918
918
  }
919
- function readClaudeConfig(path23) {
920
- if (!existsSync7(path23)) return { kind: "absent", config: {}, mtimeMs: null };
919
+ function readClaudeConfig(path24) {
920
+ if (!existsSync7(path24)) return { kind: "absent", config: {}, mtimeMs: null };
921
921
  for (let attempt = 0; attempt < 3; attempt += 1) {
922
- const before = statSync3(path23).mtimeMs;
922
+ const before = statSync3(path24).mtimeMs;
923
923
  let raw;
924
924
  try {
925
- raw = readFileSync6(path23, "utf8");
925
+ raw = readFileSync6(path24, "utf8");
926
926
  } catch {
927
927
  return { kind: "invalid", config: {}, mtimeMs: before };
928
928
  }
929
- if (!existsSync7(path23) || statSync3(path23).mtimeMs !== before) continue;
929
+ if (!existsSync7(path24) || statSync3(path24).mtimeMs !== before) continue;
930
930
  const text = raw.replace(/^\uFEFF/u, "");
931
931
  if (!text.trim()) return { kind: "empty", config: {}, mtimeMs: before };
932
932
  try {
@@ -938,9 +938,9 @@ function readClaudeConfig(path23) {
938
938
  }
939
939
  return { kind: "invalid", config: {}, mtimeMs: null };
940
940
  }
941
- function writeClaudeConfig(path23, config) {
942
- mkdirSync6(dirname4(path23), { recursive: true });
943
- writeFileAtomic(path23, `${JSON.stringify(config, null, 2)}
941
+ function writeClaudeConfig(path24, config) {
942
+ mkdirSync6(dirname4(path24), { recursive: true });
943
+ writeFileAtomic(path24, `${JSON.stringify(config, null, 2)}
944
944
  `);
945
945
  }
946
946
  function carriedEntryKeys(entry) {
@@ -3515,6 +3515,372 @@ var init_installation_token = __esm({
3515
3515
  }
3516
3516
  });
3517
3517
 
3518
+ // ../../scripts/virtual-office/code-runner/claude-credential-choice.mjs
3519
+ function isTruthyFlag(v) {
3520
+ const s = String(v ?? "").trim().toLowerCase();
3521
+ return s === "1" || s === "true" || s === "yes" || s === "on";
3522
+ }
3523
+ function wantsLogin(env2) {
3524
+ return isTruthyFlag(env2[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(env2[PREFER_LOGIN_ENV]);
3525
+ }
3526
+ function wantsKey(env2) {
3527
+ return isTruthyFlag(env2[CLAUDE_PREFER_KEY_ENV]) || isTruthyFlag(env2[PREFER_KEY_ENV]);
3528
+ }
3529
+ function classifyClaudeCredential(baseEnv = {}, { getKey, probeLogin } = {}) {
3530
+ const preferKey = wantsKey(baseEnv);
3531
+ if (!preferKey && wantsLogin(baseEnv)) {
3532
+ return { source: CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN, key: null };
3533
+ }
3534
+ if (baseEnv.ANTHROPIC_API_KEY) {
3535
+ return { source: CLAUDE_CREDENTIAL_SOURCE.ENV_KEY, key: null };
3536
+ }
3537
+ const key = getKey();
3538
+ if (!key) {
3539
+ return { source: CLAUDE_CREDENTIAL_SOURCE.NO_KEY, key: null };
3540
+ }
3541
+ if (preferKey) {
3542
+ return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY, key };
3543
+ }
3544
+ if (probeLogin() === true) {
3545
+ return { source: CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS, key: null };
3546
+ }
3547
+ return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN, key };
3548
+ }
3549
+ var PREFER_LOGIN_ENV, CLAUDE_PREFER_LOGIN_ENV, PREFER_KEY_ENV, CLAUDE_PREFER_KEY_ENV, CLAUDE_CREDENTIAL_SOURCE;
3550
+ var init_claude_credential_choice = __esm({
3551
+ "../../scripts/virtual-office/code-runner/claude-credential-choice.mjs"() {
3552
+ "use strict";
3553
+ PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
3554
+ CLAUDE_PREFER_LOGIN_ENV = "VO_RUNNER_CLAUDE_PREFER_LOGIN";
3555
+ PREFER_KEY_ENV = "VO_RUNNER_PREFER_KEY";
3556
+ CLAUDE_PREFER_KEY_ENV = "VO_RUNNER_CLAUDE_PREFER_KEY";
3557
+ CLAUDE_CREDENTIAL_SOURCE = Object.freeze({
3558
+ /** PREFER_LOGIN set (and not overridden): any API key is ignored. */
3559
+ PREFER_LOGIN: "prefer_login",
3560
+ /** An explicit ANTHROPIC_API_KEY in the environment — the manual override. */
3561
+ ENV_KEY: "env_key",
3562
+ /** No key anywhere; the spawn falls through to the login session. */
3563
+ NO_KEY: "no_key",
3564
+ /** A stored key, used because the operator explicitly opted out of tier 1. */
3565
+ KEYCHAIN_PREFER_KEY: "keychain_prefer_key",
3566
+ /** A stored key exists but a proven live subscription outranks it. */
3567
+ SUBSCRIPTION_WINS: "subscription_wins",
3568
+ /** A stored key, used because no live subscription was proven. */
3569
+ KEYCHAIN: "keychain"
3570
+ });
3571
+ }
3572
+ });
3573
+
3574
+ // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
3575
+ import { existsSync as existsSync8, realpathSync } from "node:fs";
3576
+ import { win32 as path12 } from "node:path";
3577
+ import { spawnSync } from "node:child_process";
3578
+ function pathValue(env2) {
3579
+ for (const key of ["Path", "PATH", "path"]) {
3580
+ if (typeof env2?.[key] === "string") return env2[key];
3581
+ }
3582
+ return "";
3583
+ }
3584
+ function cleanPathSegment(value) {
3585
+ const trimmed = String(value || "").trim();
3586
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
3587
+ }
3588
+ function envValue(env2, name) {
3589
+ const exact = env2?.[name];
3590
+ if (typeof exact === "string") return exact.trim();
3591
+ const key = Object.keys(env2 || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
3592
+ return typeof env2?.[key] === "string" ? env2[key].trim() : "";
3593
+ }
3594
+ function userClaudeCandidates(bin, env2) {
3595
+ if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
3596
+ const userProfile = envValue(env2, "USERPROFILE");
3597
+ const appData = envValue(env2, "APPDATA") || (userProfile ? path12.join(userProfile, "AppData", "Roaming") : "");
3598
+ const localAppData = envValue(env2, "LOCALAPPDATA") || (userProfile ? path12.join(userProfile, "AppData", "Local") : "");
3599
+ const candidates = [];
3600
+ if (appData) {
3601
+ const npmBin = path12.join(appData, "npm");
3602
+ candidates.push(
3603
+ path12.join(npmBin, "claude.exe"),
3604
+ path12.join(npmBin, "claude.cmd"),
3605
+ path12.join(npmBin, "claude.ps1"),
3606
+ path12.join(npmBin, "claude"),
3607
+ path12.join(npmBin, ...NATIVE_CLAUDE_PARTS)
3608
+ );
3609
+ }
3610
+ if (userProfile) candidates.push(path12.join(userProfile, ".local", "bin", "claude.exe"));
3611
+ if (localAppData) {
3612
+ candidates.push(
3613
+ path12.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
3614
+ path12.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
3615
+ );
3616
+ }
3617
+ return candidates;
3618
+ }
3619
+ function pathCandidates(bin, env2) {
3620
+ if (path12.isAbsolute(bin) || /[\\/]/u.test(bin)) {
3621
+ return [path12.resolve(bin)];
3622
+ }
3623
+ const extension = path12.extname(bin);
3624
+ const fromPath = pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path12.join(directory, bin)] : [
3625
+ path12.join(directory, `${bin}.exe`),
3626
+ path12.join(directory, `${bin}.cmd`),
3627
+ path12.join(directory, `${bin}.ps1`),
3628
+ path12.join(directory, bin)
3629
+ ]);
3630
+ const seen = /* @__PURE__ */ new Set();
3631
+ return [...fromPath, ...userClaudeCandidates(bin, env2)].filter((candidate) => {
3632
+ const key = candidate.toLowerCase();
3633
+ if (seen.has(key)) return false;
3634
+ seen.add(key);
3635
+ return true;
3636
+ });
3637
+ }
3638
+ function canonicalExistingPath(candidate, exists, canonicalize) {
3639
+ if (!exists(candidate)) return null;
3640
+ try {
3641
+ return canonicalize(candidate);
3642
+ } catch {
3643
+ return null;
3644
+ }
3645
+ }
3646
+ function resolveWindowsClaudeExecutable({
3647
+ bin = "claude",
3648
+ env: env2 = process.env,
3649
+ exists = existsSync8,
3650
+ canonicalize = realpathSync
3651
+ } = {}) {
3652
+ const requested = String(bin || "").trim();
3653
+ if (!requested || requested.includes("\0")) {
3654
+ throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
3655
+ }
3656
+ for (const candidate of pathCandidates(requested, env2)) {
3657
+ const found = canonicalExistingPath(candidate, exists, canonicalize);
3658
+ if (!found) continue;
3659
+ if (path12.extname(found).toLowerCase() === ".exe") return found;
3660
+ const native = path12.join(path12.dirname(found), ...NATIVE_CLAUDE_PARTS);
3661
+ const resolvedNative = canonicalExistingPath(native, exists, canonicalize);
3662
+ if (resolvedNative) return resolvedNative;
3663
+ }
3664
+ const error = new Error(
3665
+ `Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
3666
+ );
3667
+ error.code = "ENOENT";
3668
+ throw error;
3669
+ }
3670
+ function buildWindowsClaudeLaunch({
3671
+ bin = "claude",
3672
+ args = [],
3673
+ env: env2 = process.env
3674
+ } = {}) {
3675
+ return {
3676
+ bin: resolveWindowsClaudeExecutable({ bin, env: env2 }),
3677
+ args: Array.from(args, (value) => String(value)),
3678
+ spawnOptions: {
3679
+ shell: false,
3680
+ windowsHide: true,
3681
+ windowsVerbatimArguments: false
3682
+ }
3683
+ };
3684
+ }
3685
+ function spawnClaudeSync(args = [], options = {}) {
3686
+ if (process.platform !== "win32") {
3687
+ return spawnSync("claude", args, { windowsHide: true, ...options });
3688
+ }
3689
+ try {
3690
+ const launch = buildWindowsClaudeLaunch({
3691
+ bin: "claude",
3692
+ args,
3693
+ env: options.env || process.env
3694
+ });
3695
+ return spawnSync(launch.bin, launch.args, {
3696
+ ...options,
3697
+ ...launch.spawnOptions
3698
+ });
3699
+ } catch (error) {
3700
+ return {
3701
+ error,
3702
+ status: null,
3703
+ signal: null,
3704
+ output: null,
3705
+ stdout: null,
3706
+ stderr: null
3707
+ };
3708
+ }
3709
+ }
3710
+ var NATIVE_CLAUDE_PARTS;
3711
+ var init_windows_claude_launch = __esm({
3712
+ "../../scripts/virtual-office/code-runner/windows-claude-launch.mjs"() {
3713
+ "use strict";
3714
+ NATIVE_CLAUDE_PARTS = [
3715
+ "node_modules",
3716
+ "@anthropic-ai",
3717
+ "claude-code",
3718
+ "bin",
3719
+ "claude.exe"
3720
+ ];
3721
+ }
3722
+ });
3723
+
3724
+ // ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
3725
+ import { createRequire as createRequire2 } from "node:module";
3726
+ import { spawnSync as spawnSync2 } from "node:child_process";
3727
+ function defaultEntryCtor() {
3728
+ if (_loadTried) return _entryCtor;
3729
+ _loadTried = true;
3730
+ try {
3731
+ _entryCtor = require2("@napi-rs/keyring").Entry;
3732
+ } catch {
3733
+ _entryCtor = null;
3734
+ }
3735
+ return _entryCtor;
3736
+ }
3737
+ function getAnthropicKey({ EntryCtor = defaultEntryCtor() } = {}) {
3738
+ if (!EntryCtor) return null;
3739
+ try {
3740
+ return new EntryCtor(KEY_SERVICE, KEY_ACCOUNT).getPassword() || null;
3741
+ } catch {
3742
+ return null;
3743
+ }
3744
+ }
3745
+ function hasAnthropicKey(opts = {}) {
3746
+ return getAnthropicKey(opts) !== null;
3747
+ }
3748
+ function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
3749
+ const { source, key } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
3750
+ const next = { ...baseEnv };
3751
+ if (source === CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN) {
3752
+ delete next.ANTHROPIC_API_KEY;
3753
+ return next;
3754
+ }
3755
+ if (key !== null) next.ANTHROPIC_API_KEY = key;
3756
+ return next;
3757
+ }
3758
+ function claudeCostBasis(env2 = process.env) {
3759
+ return String(env2.ANTHROPIC_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
3760
+ }
3761
+ function describeAnthropicAuthSource(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
3762
+ const { source } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
3763
+ return AUTH_SOURCE_DESCRIPTION[source];
3764
+ }
3765
+ function augmentAuthError(summary) {
3766
+ const s = String(summary ?? "");
3767
+ if (!AUTH_ERROR_RE.test(s)) return s;
3768
+ return `${s}
3769
+ \u21B3 Anthropic auth failed on the runner. The \`claude\` CLI is a SEPARATE install/login from the Claude Desktop app and the Claude Code IDE extension \u2014 signing into those does NOT authenticate it. Fix: run \`claude auth login\` (Claude subscription) on the runner machine, or clear any stale ANTHROPIC_API_KEY (env / OS keychain / .env.local) and set VO_RUNNER_PREFER_LOGIN=1 \u2014 then restart the runner. Verify with \`claude -p "say hi"\`.`;
3770
+ }
3771
+ function probeClaudeLoginState({
3772
+ spawn: spawn5 = spawnSync2,
3773
+ buildWindowsLaunch = buildWindowsClaudeLaunch,
3774
+ platform: platform4 = process.platform
3775
+ } = {}) {
3776
+ try {
3777
+ const launch = platform4 === "win32" ? buildWindowsLaunch({ bin: "claude", args: ["auth", "status"] }) : { bin: "claude", args: ["auth", "status"], spawnOptions: { windowsHide: true } };
3778
+ const st = spawn5(launch.bin, launch.args, { ...launch.spawnOptions, timeout: 5e3, encoding: "utf8" });
3779
+ const parsed = JSON.parse(String(st.stdout || "").trim() || "{}");
3780
+ return typeof parsed.loggedIn === "boolean" ? parsed.loggedIn : null;
3781
+ } catch {
3782
+ return null;
3783
+ }
3784
+ }
3785
+ var require2, KEY_SERVICE, KEY_ACCOUNT, _entryCtor, _loadTried, AUTH_SOURCE_DESCRIPTION, AUTH_ERROR_RE;
3786
+ var init_anthropic_key_store = __esm({
3787
+ "../../scripts/virtual-office/code-runner/anthropic-key-store.mjs"() {
3788
+ "use strict";
3789
+ init_windows_claude_launch();
3790
+ init_claude_credential_choice();
3791
+ init_claude_credential_choice();
3792
+ require2 = createRequire2(import.meta.url);
3793
+ KEY_SERVICE = "algosuite-vo";
3794
+ KEY_ACCOUNT = "anthropic-api-key";
3795
+ _loadTried = false;
3796
+ AUTH_SOURCE_DESCRIPTION = Object.freeze({
3797
+ [CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN]: "claude auth login (VO_RUNNER_PREFER_LOGIN set \u2014 any API key ignored)",
3798
+ [CLAUDE_CREDENTIAL_SOURCE.ENV_KEY]: "ANTHROPIC_API_KEY from environment",
3799
+ [CLAUDE_CREDENTIAL_SOURCE.NO_KEY]: "claude auth login session (no API key set)",
3800
+ [CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY]: "ANTHROPIC_API_KEY from OS keychain (VO_RUNNER_PREFER_KEY set \u2014 subscription ignored)",
3801
+ [CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS]: "claude auth login session (subscription beats the stored keychain key)",
3802
+ [CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN]: "ANTHROPIC_API_KEY from OS keychain"
3803
+ });
3804
+ AUTH_ERROR_RE = /\b401\b|invalid[^.]{0,24}(authentication|credential)|authentication_error|unauthorized|not[ _-]?authenticated/i;
3805
+ }
3806
+ });
3807
+
3808
+ // ../../scripts/virtual-office/code-runner/agent-auth-attestation.mjs
3809
+ function isTruthyFlag2(value) {
3810
+ const s = String(value ?? "").trim().toLowerCase();
3811
+ return s === "1" || s === "true" || s === "yes" || s === "on";
3812
+ }
3813
+ function runnerAllowsApiBilling(env2 = {}) {
3814
+ return isTruthyFlag2(env2[RUNNER_ALLOW_API_BILLING_ENV]);
3815
+ }
3816
+ function hasApiBillingCredential(env2 = {}) {
3817
+ for (const [name, value] of Object.entries(env2)) {
3818
+ if (!API_BILLING_ENV_NAME_SET.has(String(name).toUpperCase())) continue;
3819
+ if (String(value ?? "").trim()) return true;
3820
+ }
3821
+ return false;
3822
+ }
3823
+ function stripApiBillingEnv(env2 = {}) {
3824
+ const next = {};
3825
+ for (const [name, value] of Object.entries(env2)) {
3826
+ if (API_BILLING_ENV_NAME_SET.has(String(name).toUpperCase())) continue;
3827
+ next[name] = value;
3828
+ }
3829
+ return next;
3830
+ }
3831
+ function resolveAgentAuthSource(env2 = {}) {
3832
+ return hasApiBillingCredential(env2) ? AGENT_AUTH_SOURCE.API_KEY : AGENT_AUTH_SOURCE.LOGIN;
3833
+ }
3834
+ function applyApiBillingPolicy(spawnEnv = {}, { runnerEnv = spawnEnv, allowApiBilling = false } = {}) {
3835
+ const forcedLogin = wantsLogin(runnerEnv) && !wantsKey(runnerEnv);
3836
+ const permitted = !forcedLogin && allowApiBilling === true && runnerAllowsApiBilling(runnerEnv);
3837
+ const env2 = permitted ? { ...spawnEnv } : stripApiBillingEnv(spawnEnv);
3838
+ return { env: env2, agent_auth_source: resolveAgentAuthSource(env2), permitted };
3839
+ }
3840
+ function resolveRunnerAttestedAuthSource(env2 = process.env, { hasStoredKey = hasAnthropicKey } = {}) {
3841
+ if (wantsLogin(env2) && !wantsKey(env2)) return AGENT_AUTH_SOURCE.LOGIN;
3842
+ if (!runnerAllowsApiBilling(env2)) return AGENT_AUTH_SOURCE.LOGIN;
3843
+ if (hasApiBillingCredential(env2)) return AGENT_AUTH_SOURCE.API_KEY;
3844
+ try {
3845
+ return hasStoredKey() === true ? AGENT_AUTH_SOURCE.API_KEY : AGENT_AUTH_SOURCE.LOGIN;
3846
+ } catch {
3847
+ return AGENT_AUTH_SOURCE.API_KEY;
3848
+ }
3849
+ }
3850
+ function describeTaskAuthSource(env2 = process.env, task = {}) {
3851
+ const { agent_auth_source: authSource, permitted } = applyApiBillingPolicy(env2, {
3852
+ runnerEnv: env2,
3853
+ allowApiBilling: task?.allow_api_billing === true
3854
+ });
3855
+ const reason = permitted ? "explicit_opt_in" : "default";
3856
+ return `auth_source=${authSource} reason=${reason}`;
3857
+ }
3858
+ var AGENT_AUTH_SOURCE, AGENT_AUTH_SOURCES, API_BILLING_ENV_NAMES, RUNNER_ALLOW_API_BILLING_ENV, API_BILLING_ENV_NAME_SET;
3859
+ var init_agent_auth_attestation = __esm({
3860
+ "../../scripts/virtual-office/code-runner/agent-auth-attestation.mjs"() {
3861
+ "use strict";
3862
+ init_claude_credential_choice();
3863
+ init_anthropic_key_store();
3864
+ AGENT_AUTH_SOURCE = Object.freeze({
3865
+ /** A flat-cost linked account (`claude auth login`). No per-token vendor bill. */
3866
+ LOGIN: "login",
3867
+ /** A metered per-token credential the vendor bills. */
3868
+ API_KEY: "api_key"
3869
+ });
3870
+ AGENT_AUTH_SOURCES = Object.freeze([
3871
+ AGENT_AUTH_SOURCE.LOGIN,
3872
+ AGENT_AUTH_SOURCE.API_KEY
3873
+ ]);
3874
+ API_BILLING_ENV_NAMES = Object.freeze([
3875
+ "ANTHROPIC_API_KEY",
3876
+ "ANTHROPIC_AUTH_TOKEN",
3877
+ "CLAUDE_API_KEY"
3878
+ ]);
3879
+ RUNNER_ALLOW_API_BILLING_ENV = "VO_RUNNER_ALLOW_API_BILLING";
3880
+ API_BILLING_ENV_NAME_SET = new Set(API_BILLING_ENV_NAMES);
3881
+ }
3882
+ });
3883
+
3518
3884
  // ../../scripts/virtual-office/code-runner/control-plane-heartbeat-body.mjs
3519
3885
  function buildRunnerHeartbeatBody({
3520
3886
  runnerId,
@@ -3540,6 +3906,8 @@ function buildRunnerHeartbeatBody({
3540
3906
  accountUsage,
3541
3907
  availableLocalModels,
3542
3908
  supportedTaskKinds,
3909
+ hostHealth,
3910
+ agentAuthSource,
3543
3911
  prepared_job_shadow: preparedJobShadow
3544
3912
  } = {}) {
3545
3913
  const body = { runner_id: runnerId, ...preparedJobShadow ? { prepared_job_shadow: preparedJobShadow } : {} };
@@ -3577,11 +3945,14 @@ function buildRunnerHeartbeatBody({
3577
3945
  if (Array.isArray(supportedTaskKinds) && supportedTaskKinds.length > 0) {
3578
3946
  body.supported_task_kinds = supportedTaskKinds;
3579
3947
  }
3948
+ if (hostHealth && typeof hostHealth === "object") body.host_health = hostHealth;
3949
+ if (AGENT_AUTH_SOURCES.includes(agentAuthSource)) body.agent_auth_source = agentAuthSource;
3580
3950
  return body;
3581
3951
  }
3582
3952
  var init_control_plane_heartbeat_body = __esm({
3583
3953
  "../../scripts/virtual-office/code-runner/control-plane-heartbeat-body.mjs"() {
3584
3954
  "use strict";
3955
+ init_agent_auth_attestation();
3585
3956
  }
3586
3957
  });
3587
3958
 
@@ -3879,13 +4250,13 @@ async function getTaskKnowledgeContextRequest(req, taskId, { query, knowledgeReq
3879
4250
  const canonicalQuery = canonicalizeKnowledgeContextQuery(query);
3880
4251
  if (canonicalQuery.trim()) body.query = canonicalQuery;
3881
4252
  if (typeof knowledgeRequestId === "string" && knowledgeRequestId) body.knowledge_request_id = knowledgeRequestId;
3882
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
4253
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
3883
4254
  const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
3884
4255
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
3885
4256
  let res;
3886
4257
  let cause;
3887
4258
  try {
3888
- res = await req("POST", path23, body, { timeoutMs });
4259
+ res = await req("POST", path24, body, { timeoutMs });
3889
4260
  } catch (err) {
3890
4261
  cause = err;
3891
4262
  }
@@ -3953,10 +4324,10 @@ async function getPreparedJobRequest(req, taskId, options = {}, invalidateToken
3953
4324
  if (typeof taskId !== "string" || taskId.length === 0) {
3954
4325
  return { ok: false, reason: "missing_task_id", status: 0, ...envMeta };
3955
4326
  }
3956
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/prepared-job?${query}`;
4327
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/prepared-job?${query}`;
3957
4328
  let res;
3958
4329
  try {
3959
- res = await req("GET", path23, void 0, { timeoutMs });
4330
+ res = await req("GET", path24, void 0, { timeoutMs });
3960
4331
  } catch (err) {
3961
4332
  return { ok: false, reason: `transport: ${err?.message || String(err)}`, status: 0, ...envMeta };
3962
4333
  }
@@ -4056,11 +4427,11 @@ function createControlPlaneClient({
4056
4427
  if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
4057
4428
  const root = resolvedBaseUrl.replace(/\/+$/, "");
4058
4429
  const claimOccurrences = /* @__PURE__ */ new Map();
4059
- async function req(method, path23, body, { timeoutMs } = {}) {
4430
+ async function req(method, path24, body, { timeoutMs } = {}) {
4060
4431
  const bearer = await resolveBearer(env2);
4061
4432
  const controller = timeoutMs ? new AbortController() : null;
4062
4433
  let timeoutId;
4063
- const request = Promise.resolve(fetchImpl(`${root}${path23}`, {
4434
+ const request = Promise.resolve(fetchImpl(`${root}${path24}`, {
4064
4435
  method,
4065
4436
  headers: {
4066
4437
  "content-type": "application/json",
@@ -4073,7 +4444,7 @@ function createControlPlaneClient({
4073
4444
  const timeout = new Promise((_, reject) => {
4074
4445
  timeoutId = setTimeout(() => {
4075
4446
  controller.abort();
4076
- reject(new Error(`control-plane ${path23} timed out after ${timeoutMs}ms`));
4447
+ reject(new Error(`control-plane ${path24} timed out after ${timeoutMs}ms`));
4077
4448
  }, timeoutMs);
4078
4449
  });
4079
4450
  try {
@@ -4082,7 +4453,7 @@ function createControlPlaneClient({
4082
4453
  clearTimeout(timeoutId);
4083
4454
  }
4084
4455
  }
4085
- const taskReq = (method, path23, body, options = {}) => req(method, path23, body, { timeoutMs: taskRequestTimeoutMs, ...options });
4456
+ const taskReq = (method, path24, body, options = {}) => req(method, path24, body, { timeoutMs: taskRequestTimeoutMs, ...options });
4086
4457
  const claimGate = makeClaimGateNotice({ log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`) });
4087
4458
  return {
4088
4459
  getClaimGate: () => claimGate.current(),
@@ -4111,6 +4482,7 @@ function createControlPlaneClient({
4111
4482
  }
4112
4483
  if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
4113
4484
  if (session.defaultAgent) body.default_agent = session.defaultAgent;
4485
+ if (AGENT_AUTH_SOURCES.includes(session.agentAuthSource)) body.agent_auth_source = session.agentAuthSource;
4114
4486
  if (Array.isArray(session.availableAgents)) {
4115
4487
  body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
4116
4488
  }
@@ -4253,8 +4625,8 @@ function createControlPlaneClient({
4253
4625
  return listAllPrOpenedTasks(taskReq);
4254
4626
  },
4255
4627
  async downloadTaskAttachment(taskId, attachmentId) {
4256
- const path23 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
4257
- const res = await taskReq("GET", path23);
4628
+ const path24 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
4629
+ const res = await taskReq("GET", path24);
4258
4630
  if (res.status === 401) cachedFirebaseToken = null;
4259
4631
  if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
4260
4632
  return Buffer.from(await res.arrayBuffer());
@@ -4367,333 +4739,47 @@ function createControlPlaneClient({
4367
4739
  return fetchInstallationToken({ req: taskReq, required, readOnly, repo });
4368
4740
  },
4369
4741
  /**
4370
- * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
4371
- * Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'marathon'; 'ultracode' legacy),
4372
- * defaulting to 'standard' on any error. Never throws — best-effort.
4373
- */
4374
- async getDispatchMode() {
4375
- try {
4376
- const res = await taskReq("GET", "/api/v1/dispatch-mode-config");
4377
- if (!res.ok) return "standard";
4378
- const json = await res.json();
4379
- return json?.dispatchMode || "standard";
4380
- } catch {
4381
- return "standard";
4382
- }
4383
- }
4384
- };
4385
- }
4386
- var cachedFirebaseToken, ClaimAuthorityChangedError;
4387
- var init_control_plane_client = __esm({
4388
- "../../scripts/virtual-office/code-runner/control-plane-client.mjs"() {
4389
- "use strict";
4390
- init_installation_token();
4391
- init_control_plane_heartbeat_body();
4392
- init_control_plane_promote();
4393
- init_control_plane_task_list();
4394
- init_control_plane_resume();
4395
- init_control_plane_autonomous_admission();
4396
- init_control_plane_merge();
4397
- init_control_plane_weekly_tokens();
4398
- init_control_plane_telemetry_relay();
4399
- init_claim_gate_notice();
4400
- init_control_plane_knowledge_context();
4401
- init_control_plane_prepared_job();
4402
- cachedFirebaseToken = null;
4403
- ClaimAuthorityChangedError = class extends Error {
4404
- constructor() {
4405
- super("code-task claim authority changed");
4406
- this.name = "ClaimAuthorityChangedError";
4407
- this.code = "code_task_claim_authority_changed";
4408
- }
4409
- };
4410
- }
4411
- });
4412
-
4413
- // ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
4414
- import { existsSync as existsSync8, realpathSync } from "node:fs";
4415
- import { win32 as path12 } from "node:path";
4416
- import { spawnSync } from "node:child_process";
4417
- function pathValue(env2) {
4418
- for (const key of ["Path", "PATH", "path"]) {
4419
- if (typeof env2?.[key] === "string") return env2[key];
4420
- }
4421
- return "";
4422
- }
4423
- function cleanPathSegment(value) {
4424
- const trimmed = String(value || "").trim();
4425
- return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
4426
- }
4427
- function envValue(env2, name) {
4428
- const exact = env2?.[name];
4429
- if (typeof exact === "string") return exact.trim();
4430
- const key = Object.keys(env2 || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
4431
- return typeof env2?.[key] === "string" ? env2[key].trim() : "";
4432
- }
4433
- function userClaudeCandidates(bin, env2) {
4434
- if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
4435
- const userProfile = envValue(env2, "USERPROFILE");
4436
- const appData = envValue(env2, "APPDATA") || (userProfile ? path12.join(userProfile, "AppData", "Roaming") : "");
4437
- const localAppData = envValue(env2, "LOCALAPPDATA") || (userProfile ? path12.join(userProfile, "AppData", "Local") : "");
4438
- const candidates = [];
4439
- if (appData) {
4440
- const npmBin = path12.join(appData, "npm");
4441
- candidates.push(
4442
- path12.join(npmBin, "claude.exe"),
4443
- path12.join(npmBin, "claude.cmd"),
4444
- path12.join(npmBin, "claude.ps1"),
4445
- path12.join(npmBin, "claude"),
4446
- path12.join(npmBin, ...NATIVE_CLAUDE_PARTS)
4447
- );
4448
- }
4449
- if (userProfile) candidates.push(path12.join(userProfile, ".local", "bin", "claude.exe"));
4450
- if (localAppData) {
4451
- candidates.push(
4452
- path12.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
4453
- path12.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
4454
- );
4455
- }
4456
- return candidates;
4457
- }
4458
- function pathCandidates(bin, env2) {
4459
- if (path12.isAbsolute(bin) || /[\\/]/u.test(bin)) {
4460
- return [path12.resolve(bin)];
4461
- }
4462
- const extension = path12.extname(bin);
4463
- const fromPath = pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path12.join(directory, bin)] : [
4464
- path12.join(directory, `${bin}.exe`),
4465
- path12.join(directory, `${bin}.cmd`),
4466
- path12.join(directory, `${bin}.ps1`),
4467
- path12.join(directory, bin)
4468
- ]);
4469
- const seen = /* @__PURE__ */ new Set();
4470
- return [...fromPath, ...userClaudeCandidates(bin, env2)].filter((candidate) => {
4471
- const key = candidate.toLowerCase();
4472
- if (seen.has(key)) return false;
4473
- seen.add(key);
4474
- return true;
4475
- });
4476
- }
4477
- function canonicalExistingPath(candidate, exists, canonicalize) {
4478
- if (!exists(candidate)) return null;
4479
- try {
4480
- return canonicalize(candidate);
4481
- } catch {
4482
- return null;
4483
- }
4484
- }
4485
- function resolveWindowsClaudeExecutable({
4486
- bin = "claude",
4487
- env: env2 = process.env,
4488
- exists = existsSync8,
4489
- canonicalize = realpathSync
4490
- } = {}) {
4491
- const requested = String(bin || "").trim();
4492
- if (!requested || requested.includes("\0")) {
4493
- throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
4494
- }
4495
- for (const candidate of pathCandidates(requested, env2)) {
4496
- const found = canonicalExistingPath(candidate, exists, canonicalize);
4497
- if (!found) continue;
4498
- if (path12.extname(found).toLowerCase() === ".exe") return found;
4499
- const native = path12.join(path12.dirname(found), ...NATIVE_CLAUDE_PARTS);
4500
- const resolvedNative = canonicalExistingPath(native, exists, canonicalize);
4501
- if (resolvedNative) return resolvedNative;
4502
- }
4503
- const error = new Error(
4504
- `Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
4505
- );
4506
- error.code = "ENOENT";
4507
- throw error;
4508
- }
4509
- function buildWindowsClaudeLaunch({
4510
- bin = "claude",
4511
- args = [],
4512
- env: env2 = process.env
4513
- } = {}) {
4514
- return {
4515
- bin: resolveWindowsClaudeExecutable({ bin, env: env2 }),
4516
- args: Array.from(args, (value) => String(value)),
4517
- spawnOptions: {
4518
- shell: false,
4519
- windowsHide: true,
4520
- windowsVerbatimArguments: false
4742
+ * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
4743
+ * Returns the mode string ('fast'|'standard'|'deep'|'ultra'|'marathon'; 'ultracode' legacy),
4744
+ * defaulting to 'standard' on any error. Never throws — best-effort.
4745
+ */
4746
+ async getDispatchMode() {
4747
+ try {
4748
+ const res = await taskReq("GET", "/api/v1/dispatch-mode-config");
4749
+ if (!res.ok) return "standard";
4750
+ const json = await res.json();
4751
+ return json?.dispatchMode || "standard";
4752
+ } catch {
4753
+ return "standard";
4754
+ }
4521
4755
  }
4522
4756
  };
4523
4757
  }
4524
- function spawnClaudeSync(args = [], options = {}) {
4525
- if (process.platform !== "win32") {
4526
- return spawnSync("claude", args, { windowsHide: true, ...options });
4527
- }
4528
- try {
4529
- const launch = buildWindowsClaudeLaunch({
4530
- bin: "claude",
4531
- args,
4532
- env: options.env || process.env
4533
- });
4534
- return spawnSync(launch.bin, launch.args, {
4535
- ...options,
4536
- ...launch.spawnOptions
4537
- });
4538
- } catch (error) {
4539
- return {
4540
- error,
4541
- status: null,
4542
- signal: null,
4543
- output: null,
4544
- stdout: null,
4545
- stderr: null
4546
- };
4547
- }
4548
- }
4549
- var NATIVE_CLAUDE_PARTS;
4550
- var init_windows_claude_launch = __esm({
4551
- "../../scripts/virtual-office/code-runner/windows-claude-launch.mjs"() {
4552
- "use strict";
4553
- NATIVE_CLAUDE_PARTS = [
4554
- "node_modules",
4555
- "@anthropic-ai",
4556
- "claude-code",
4557
- "bin",
4558
- "claude.exe"
4559
- ];
4560
- }
4561
- });
4562
-
4563
- // ../../scripts/virtual-office/code-runner/claude-credential-choice.mjs
4564
- function isTruthyFlag(v) {
4565
- const s = String(v ?? "").trim().toLowerCase();
4566
- return s === "1" || s === "true" || s === "yes" || s === "on";
4567
- }
4568
- function wantsLogin(env2) {
4569
- return isTruthyFlag(env2[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(env2[PREFER_LOGIN_ENV]);
4570
- }
4571
- function wantsKey(env2) {
4572
- return isTruthyFlag(env2[CLAUDE_PREFER_KEY_ENV]) || isTruthyFlag(env2[PREFER_KEY_ENV]);
4573
- }
4574
- function classifyClaudeCredential(baseEnv = {}, { getKey, probeLogin } = {}) {
4575
- const preferKey = wantsKey(baseEnv);
4576
- if (!preferKey && wantsLogin(baseEnv)) {
4577
- return { source: CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN, key: null };
4578
- }
4579
- if (baseEnv.ANTHROPIC_API_KEY) {
4580
- return { source: CLAUDE_CREDENTIAL_SOURCE.ENV_KEY, key: null };
4581
- }
4582
- const key = getKey();
4583
- if (!key) {
4584
- return { source: CLAUDE_CREDENTIAL_SOURCE.NO_KEY, key: null };
4585
- }
4586
- if (preferKey) {
4587
- return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY, key };
4588
- }
4589
- if (probeLogin() === true) {
4590
- return { source: CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS, key: null };
4591
- }
4592
- return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN, key };
4593
- }
4594
- var PREFER_LOGIN_ENV, CLAUDE_PREFER_LOGIN_ENV, PREFER_KEY_ENV, CLAUDE_PREFER_KEY_ENV, CLAUDE_CREDENTIAL_SOURCE;
4595
- var init_claude_credential_choice = __esm({
4596
- "../../scripts/virtual-office/code-runner/claude-credential-choice.mjs"() {
4597
- "use strict";
4598
- PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
4599
- CLAUDE_PREFER_LOGIN_ENV = "VO_RUNNER_CLAUDE_PREFER_LOGIN";
4600
- PREFER_KEY_ENV = "VO_RUNNER_PREFER_KEY";
4601
- CLAUDE_PREFER_KEY_ENV = "VO_RUNNER_CLAUDE_PREFER_KEY";
4602
- CLAUDE_CREDENTIAL_SOURCE = Object.freeze({
4603
- /** PREFER_LOGIN set (and not overridden): any API key is ignored. */
4604
- PREFER_LOGIN: "prefer_login",
4605
- /** An explicit ANTHROPIC_API_KEY in the environment — the manual override. */
4606
- ENV_KEY: "env_key",
4607
- /** No key anywhere; the spawn falls through to the login session. */
4608
- NO_KEY: "no_key",
4609
- /** A stored key, used because the operator explicitly opted out of tier 1. */
4610
- KEYCHAIN_PREFER_KEY: "keychain_prefer_key",
4611
- /** A stored key exists but a proven live subscription outranks it. */
4612
- SUBSCRIPTION_WINS: "subscription_wins",
4613
- /** A stored key, used because no live subscription was proven. */
4614
- KEYCHAIN: "keychain"
4615
- });
4616
- }
4617
- });
4618
-
4619
- // ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
4620
- import { createRequire as createRequire2 } from "node:module";
4621
- import { spawnSync as spawnSync2 } from "node:child_process";
4622
- function defaultEntryCtor() {
4623
- if (_loadTried) return _entryCtor;
4624
- _loadTried = true;
4625
- try {
4626
- _entryCtor = require2("@napi-rs/keyring").Entry;
4627
- } catch {
4628
- _entryCtor = null;
4629
- }
4630
- return _entryCtor;
4631
- }
4632
- function getAnthropicKey({ EntryCtor = defaultEntryCtor() } = {}) {
4633
- if (!EntryCtor) return null;
4634
- try {
4635
- return new EntryCtor(KEY_SERVICE, KEY_ACCOUNT).getPassword() || null;
4636
- } catch {
4637
- return null;
4638
- }
4639
- }
4640
- function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
4641
- const { source, key } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
4642
- const next = { ...baseEnv };
4643
- if (source === CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN) {
4644
- delete next.ANTHROPIC_API_KEY;
4645
- return next;
4646
- }
4647
- if (key !== null) next.ANTHROPIC_API_KEY = key;
4648
- return next;
4649
- }
4650
- function claudeCostBasis(env2 = process.env) {
4651
- return String(env2.ANTHROPIC_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
4652
- }
4653
- function describeAnthropicAuthSource(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
4654
- const { source } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
4655
- return AUTH_SOURCE_DESCRIPTION[source];
4656
- }
4657
- function augmentAuthError(summary) {
4658
- const s = String(summary ?? "");
4659
- if (!AUTH_ERROR_RE.test(s)) return s;
4660
- return `${s}
4661
- \u21B3 Anthropic auth failed on the runner. The \`claude\` CLI is a SEPARATE install/login from the Claude Desktop app and the Claude Code IDE extension \u2014 signing into those does NOT authenticate it. Fix: run \`claude auth login\` (Claude subscription) on the runner machine, or clear any stale ANTHROPIC_API_KEY (env / OS keychain / .env.local) and set VO_RUNNER_PREFER_LOGIN=1 \u2014 then restart the runner. Verify with \`claude -p "say hi"\`.`;
4662
- }
4663
- function probeClaudeLoginState({
4664
- spawn: spawn5 = spawnSync2,
4665
- buildWindowsLaunch = buildWindowsClaudeLaunch,
4666
- platform: platform4 = process.platform
4667
- } = {}) {
4668
- try {
4669
- const launch = platform4 === "win32" ? buildWindowsLaunch({ bin: "claude", args: ["auth", "status"] }) : { bin: "claude", args: ["auth", "status"], spawnOptions: { windowsHide: true } };
4670
- const st = spawn5(launch.bin, launch.args, { ...launch.spawnOptions, timeout: 5e3, encoding: "utf8" });
4671
- const parsed = JSON.parse(String(st.stdout || "").trim() || "{}");
4672
- return typeof parsed.loggedIn === "boolean" ? parsed.loggedIn : null;
4673
- } catch {
4674
- return null;
4675
- }
4676
- }
4677
- var require2, KEY_SERVICE, KEY_ACCOUNT, _entryCtor, _loadTried, AUTH_SOURCE_DESCRIPTION, AUTH_ERROR_RE;
4678
- var init_anthropic_key_store = __esm({
4679
- "../../scripts/virtual-office/code-runner/anthropic-key-store.mjs"() {
4758
+ var cachedFirebaseToken, ClaimAuthorityChangedError;
4759
+ var init_control_plane_client = __esm({
4760
+ "../../scripts/virtual-office/code-runner/control-plane-client.mjs"() {
4680
4761
  "use strict";
4681
- init_windows_claude_launch();
4682
- init_claude_credential_choice();
4683
- init_claude_credential_choice();
4684
- require2 = createRequire2(import.meta.url);
4685
- KEY_SERVICE = "algosuite-vo";
4686
- KEY_ACCOUNT = "anthropic-api-key";
4687
- _loadTried = false;
4688
- AUTH_SOURCE_DESCRIPTION = Object.freeze({
4689
- [CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN]: "claude auth login (VO_RUNNER_PREFER_LOGIN set \u2014 any API key ignored)",
4690
- [CLAUDE_CREDENTIAL_SOURCE.ENV_KEY]: "ANTHROPIC_API_KEY from environment",
4691
- [CLAUDE_CREDENTIAL_SOURCE.NO_KEY]: "claude auth login session (no API key set)",
4692
- [CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY]: "ANTHROPIC_API_KEY from OS keychain (VO_RUNNER_PREFER_KEY set \u2014 subscription ignored)",
4693
- [CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS]: "claude auth login session (subscription beats the stored keychain key)",
4694
- [CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN]: "ANTHROPIC_API_KEY from OS keychain"
4695
- });
4696
- AUTH_ERROR_RE = /\b401\b|invalid[^.]{0,24}(authentication|credential)|authentication_error|unauthorized|not[ _-]?authenticated/i;
4762
+ init_installation_token();
4763
+ init_control_plane_heartbeat_body();
4764
+ init_control_plane_promote();
4765
+ init_control_plane_task_list();
4766
+ init_control_plane_resume();
4767
+ init_control_plane_autonomous_admission();
4768
+ init_control_plane_merge();
4769
+ init_control_plane_weekly_tokens();
4770
+ init_control_plane_telemetry_relay();
4771
+ init_claim_gate_notice();
4772
+ init_agent_auth_attestation();
4773
+ init_control_plane_knowledge_context();
4774
+ init_control_plane_prepared_job();
4775
+ cachedFirebaseToken = null;
4776
+ ClaimAuthorityChangedError = class extends Error {
4777
+ constructor() {
4778
+ super("code-task claim authority changed");
4779
+ this.name = "ClaimAuthorityChangedError";
4780
+ this.code = "code_task_claim_authority_changed";
4781
+ }
4782
+ };
4697
4783
  }
4698
4784
  });
4699
4785
 
@@ -4907,7 +4993,7 @@ function normalizeClaudePermissionMode(value) {
4907
4993
  }
4908
4994
  return normalized;
4909
4995
  }
4910
- function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", env: env2 = process.env } = {}) {
4996
+ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", structuredOutputSchema, env: env2 = process.env } = {}) {
4911
4997
  const effectivePermissionMode = normalizeClaudePermissionMode(permissionMode);
4912
4998
  if (!["default", "skill_readonly", "frozen_inputs_only"].includes(toolPolicy)) {
4913
4999
  throw new Error(`unsupported Claude tool policy "${toolPolicy}"`);
@@ -4945,6 +5031,12 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
4945
5031
  "none"
4946
5032
  );
4947
5033
  }
5034
+ if (structuredOutputSchema !== void 0) {
5035
+ if (!restrictedSkill || !structuredOutputSchema || typeof structuredOutputSchema !== "object" || Array.isArray(structuredOutputSchema)) {
5036
+ throw new Error("structured output schema is allowed only for a restricted skill");
5037
+ }
5038
+ args.push("--json-schema", JSON.stringify(structuredOutputSchema));
5039
+ }
4948
5040
  if (frozenInputsOnly) {
4949
5041
  args.push("--strict-mcp-config", "--safe-mode");
4950
5042
  }
@@ -5370,13 +5462,15 @@ function cappedRunLastMessage(evt, lastProgress) {
5370
5462
  return salvage;
5371
5463
  }
5372
5464
  function buildResultEvent(evt) {
5373
- const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
5465
+ const isError = Boolean(evt.is_error) || evt.subtype === "error_max_budget_usd" || evt.subtype === "error_max_turns" || evt.subtype === "error_max_structured_output_retries" || evt.subtype === "error_during_execution";
5374
5466
  return {
5375
5467
  kind: "result",
5376
5468
  isError,
5377
5469
  costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
5378
5470
  summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
5471
+ terminalSubtype: typeof evt.subtype === "string" ? evt.subtype : null,
5379
5472
  numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null,
5473
+ structuredOutput: Object.hasOwn(evt, "structured_output") ? evt.structured_output : null,
5380
5474
  tokenUsage: extractTokenUsage(evt),
5381
5475
  modelUsage: extractModelUsage(evt)
5382
5476
  };
@@ -5515,8 +5609,150 @@ var MIN_CLAUDE_CLI_VERSION, SECURITY_RATIONALE;
5515
5609
  var init_cli_version_floor = __esm({
5516
5610
  "../../scripts/virtual-office/code-runner/cli-version-floor.mjs"() {
5517
5611
  "use strict";
5518
- MIN_CLAUDE_CLI_VERSION = "2.1.218";
5519
- SECURITY_RATIONALE = "Claude Code 2.1.211/2.1.213 fixed a PreToolUse-hook bypass on unsandboxed Bash (our destructive-fs/git/cloud tripwires DO NOT FIRE on older CLIs) and worktree-subagents mutating the main checkout. 2.1.218 fixed Windows paths with a lowercase-\\u segment (e.g. ...\\utils\\, ...\\ui\\) being corrupted into CJK in tool inputs, making those files silently inaccessible \u2014 the fleet is Windows and 1,376 tracked files sit under utils/ alone. Update: npm install -g @anthropic-ai/claude-code (or the native installer).";
5612
+ MIN_CLAUDE_CLI_VERSION = "2.1.218";
5613
+ SECURITY_RATIONALE = "Claude Code 2.1.211/2.1.213 fixed a PreToolUse-hook bypass on unsandboxed Bash (our destructive-fs/git/cloud tripwires DO NOT FIRE on older CLIs) and worktree-subagents mutating the main checkout. 2.1.218 fixed Windows paths with a lowercase-\\u segment (e.g. ...\\utils\\, ...\\ui\\) being corrupted into CJK in tool inputs, making those files silently inaccessible \u2014 the fleet is Windows and 1,376 tracked files sit under utils/ alone. Update: npm install -g @anthropic-ai/claude-code (or the native installer).";
5614
+ }
5615
+ });
5616
+
5617
+ // ../../scripts/virtual-office/code-runner/claude-skill-capability.mjs
5618
+ import { spawnSync as spawnSync6 } from "node:child_process";
5619
+ import { accessSync, constants, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
5620
+ import path14 from "node:path";
5621
+ function hasOption(help, option) {
5622
+ const literal = option.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
5623
+ return new RegExp(`(^|\\s)${literal}(?=\\s|,|=|<|$)`, "mu").test(help);
5624
+ }
5625
+ function assessClaudeSkillCapability({ versionOutput, helpOutput }) {
5626
+ const version = parseCliVersion(versionOutput);
5627
+ if (!version || !VALIDATED_CLAUDE_SKILL_VERSIONS.includes(version)) {
5628
+ return { compatible: false, version, reason: "claude version is not in the validated restricted-skill manifest" };
5629
+ }
5630
+ const help = String(helpOutput ?? "");
5631
+ const missing = REQUIRED_CLAUDE_SKILL_HELP.filter((option) => !hasOption(help, option));
5632
+ if (missing.length > 0) {
5633
+ return { compatible: false, version, reason: `claude help is missing required options: ${missing.join(", ")}` };
5634
+ }
5635
+ if (!/--permission-prompts[\s\S]{0,300}(?:"none"|\bnone\b)/mu.test(help) || !/--output-format[\s\S]{0,300}\bstream-json\b/mu.test(help)) {
5636
+ return { compatible: false, version, reason: "claude help does not prove required none/stream-json values" };
5637
+ }
5638
+ return { compatible: true, version, reason: "validated restricted-skill CLI contract" };
5639
+ }
5640
+ function runProbe(bin, args, env2, timeoutMs = PROBE_TIMEOUT_MS) {
5641
+ if (process.platform === "win32") {
5642
+ try {
5643
+ const launch = buildWindowsClaudeLaunch({ bin, args, env: env2 });
5644
+ return spawnSync6(launch.bin, launch.args, {
5645
+ ...launch.spawnOptions,
5646
+ env: env2,
5647
+ encoding: "utf8",
5648
+ timeout: timeoutMs
5649
+ });
5650
+ } catch (error) {
5651
+ return { status: null, stdout: "", stderr: "", error };
5652
+ }
5653
+ }
5654
+ return spawnSync6(bin, args, { env: env2, encoding: "utf8", timeout: timeoutMs, windowsHide: true });
5655
+ }
5656
+ function probeText(probe) {
5657
+ return `${String(probe?.stdout ?? "")}
5658
+ ${String(probe?.stderr ?? "")}`.trim();
5659
+ }
5660
+ function resolveClaudeBinaryIdentity(bin = "claude", env2 = process.env) {
5661
+ let resolvedBin = String(bin);
5662
+ try {
5663
+ if (process.platform === "win32") {
5664
+ resolvedBin = buildWindowsClaudeLaunch({ bin: resolvedBin, args: [], env: env2 }).bin;
5665
+ } else if (!path14.isAbsolute(resolvedBin)) {
5666
+ const found = String(env2?.PATH ?? "").split(path14.delimiter).find((dir) => {
5667
+ try {
5668
+ accessSync(path14.join(dir, resolvedBin), constants.X_OK);
5669
+ return true;
5670
+ } catch {
5671
+ return false;
5672
+ }
5673
+ });
5674
+ if (found) resolvedBin = path14.join(found, resolvedBin);
5675
+ }
5676
+ const canonical = realpathSync2(resolvedBin);
5677
+ const stat3 = statSync4(canonical);
5678
+ return { resolvedBin: canonical, fingerprint: `${canonical}\0${stat3.size}\0${stat3.mtimeMs}` };
5679
+ } catch {
5680
+ const pathValue2 = String(env2?.PATH ?? env2?.Path ?? "");
5681
+ return { resolvedBin, fingerprint: `${resolvedBin}\0${pathValue2}` };
5682
+ }
5683
+ }
5684
+ function probeClaudeSkillCapability({
5685
+ bin = "claude",
5686
+ env: env2 = process.env,
5687
+ versionOutput,
5688
+ spawnProbe = runProbe,
5689
+ now = () => Date.now(),
5690
+ cacheTtlMs = CACHE_TTL_MS,
5691
+ timeoutMs = PROBE_TIMEOUT_MS,
5692
+ freshIdentity = false,
5693
+ resolveIdentity = resolveClaudeBinaryIdentity
5694
+ } = {}) {
5695
+ const identity = resolveIdentity(bin, env2);
5696
+ const key = identity.fingerprint;
5697
+ const existing = cache.get(key);
5698
+ if (!freshIdentity && versionOutput === void 0 && existing && now() - existing.at < cacheTtlMs) {
5699
+ return existing.value;
5700
+ }
5701
+ const versionProbe = freshIdentity || versionOutput === void 0 ? spawnProbe(identity.resolvedBin, ["--version"], env2, timeoutMs) : null;
5702
+ if (versionProbe?.error || versionProbe && versionProbe.status !== 0) {
5703
+ return {
5704
+ compatible: false,
5705
+ version: null,
5706
+ resolvedBin: identity.resolvedBin,
5707
+ reason: "claude version capability probe failed"
5708
+ };
5709
+ }
5710
+ const effectiveVersionOutput = versionProbe ? probeText(versionProbe) : versionOutput;
5711
+ const suppliedVersion = parseCliVersion(effectiveVersionOutput);
5712
+ if (existing && now() - existing.at < cacheTtlMs && suppliedVersion === existing.value.version) {
5713
+ return existing.value;
5714
+ }
5715
+ const helpProbe = spawnProbe(identity.resolvedBin, ["--help"], env2, timeoutMs);
5716
+ if (helpProbe?.error || helpProbe?.status !== 0) {
5717
+ return {
5718
+ compatible: false,
5719
+ version: suppliedVersion,
5720
+ resolvedBin: identity.resolvedBin,
5721
+ reason: "claude help capability probe failed"
5722
+ };
5723
+ }
5724
+ const assessed = assessClaudeSkillCapability({
5725
+ versionOutput: effectiveVersionOutput,
5726
+ helpOutput: probeText(helpProbe)
5727
+ });
5728
+ const value = { ...assessed, resolvedBin: identity.resolvedBin };
5729
+ cache.set(key, { at: now(), value });
5730
+ return value;
5731
+ }
5732
+ var VALIDATED_CLAUDE_SKILL_VERSIONS, REQUIRED_CLAUDE_SKILL_HELP, PROBE_TIMEOUT_MS, CACHE_TTL_MS, cache;
5733
+ var init_claude_skill_capability = __esm({
5734
+ "../../scripts/virtual-office/code-runner/claude-skill-capability.mjs"() {
5735
+ "use strict";
5736
+ init_cli_version_floor();
5737
+ init_windows_claude_launch();
5738
+ VALIDATED_CLAUDE_SKILL_VERSIONS = Object.freeze(["2.1.263"]);
5739
+ REQUIRED_CLAUDE_SKILL_HELP = Object.freeze([
5740
+ "--allowedTools",
5741
+ "--disable-slash-commands",
5742
+ "--json-schema",
5743
+ "--max-budget-usd",
5744
+ "--no-chrome",
5745
+ "--no-session-persistence",
5746
+ "--output-format",
5747
+ "--permission-mode",
5748
+ "--permission-prompts",
5749
+ "--safe-mode",
5750
+ "--strict-mcp-config",
5751
+ "--tools"
5752
+ ]);
5753
+ PROBE_TIMEOUT_MS = 2e3;
5754
+ CACHE_TTL_MS = 5 * 60 * 1e3;
5755
+ cache = /* @__PURE__ */ new Map();
5520
5756
  }
5521
5757
  });
5522
5758
 
@@ -5546,9 +5782,12 @@ async function checkClaudeAuth({
5546
5782
  spawnVersion = spawnClaudeSync,
5547
5783
  probeLogin = probeClaudeLoginState,
5548
5784
  getStoredKey = getAnthropicKey,
5549
- env: env2 = process.env
5785
+ probeSkillCapability = probeClaudeSkillCapability,
5786
+ env: env2 = process.env,
5787
+ now = () => Date.now()
5550
5788
  } = {}) {
5551
5789
  try {
5790
+ const startedAt = now();
5552
5791
  let probe = spawnVersion(["--version"], {
5553
5792
  timeout: FIRST_VERSION_TIMEOUT_MS,
5554
5793
  encoding: "utf8",
@@ -5582,23 +5821,40 @@ async function checkClaudeAuth({
5582
5821
  }
5583
5822
  const floorGate = applyCliVersionFloor({ versionOutput: probe.stdout, env: env2 });
5584
5823
  if (floorGate.refused) {
5585
- return { installed: true, authenticated: false, message: floorGate.message };
5824
+ return {
5825
+ installed: true,
5826
+ authenticated: false,
5827
+ version: floorGate.check.version ?? void 0,
5828
+ skillCapable: false,
5829
+ message: floorGate.message
5830
+ };
5586
5831
  }
5587
5832
  const loggedIn = retriedAfterTimeout ? null : probeLogin();
5588
5833
  if (loggedIn === false) {
5589
5834
  return {
5590
5835
  installed: true,
5591
5836
  authenticated: false,
5837
+ version: floorGate.check.version ?? void 0,
5838
+ skillCapable: false,
5592
5839
  message: "claude CLI is installed but NOT logged in \u2014 its login is SEPARATE from the Claude Desktop app and the Claude Code IDE extension. Run: claude auth login (Claude subscription), then restart the runner."
5593
5840
  };
5594
5841
  }
5842
+ const authTier = resolveClaudeAuthTier({ env: env2, loggedIn, getStoredKey });
5843
+ const remainingMs = AUTH_PROBE_BUDGET_MS - (now() - startedAt);
5844
+ const skillCapability = loggedIn === true && remainingMs >= MIN_SKILL_PROBE_MS ? probeSkillCapability({
5845
+ versionOutput: probe.stdout,
5846
+ env: env2,
5847
+ timeoutMs: Math.min(2e3, remainingMs)
5848
+ }) : { compatible: false };
5595
5849
  return {
5596
5850
  installed: true,
5597
5851
  authenticated: true,
5852
+ version: floorGate.check.version ?? void 0,
5853
+ skillCapable: skillCapability.compatible === true,
5598
5854
  // Dispatch-time billing signal, carried on the same probe that already
5599
5855
  // paid for the login read. Never sent for a non-authenticated result:
5600
5856
  // there is no tier without a working credential.
5601
- authTier: resolveClaudeAuthTier({ env: env2, loggedIn, getStoredKey }),
5857
+ authTier,
5602
5858
  message: loggedIn === true ? "claude CLI installed and logged in (claude auth status)" : "claude binary found (login state unknown \u2014 auth check is best-effort)"
5603
5859
  };
5604
5860
  } catch (error) {
@@ -5609,7 +5865,7 @@ async function checkClaudeAuth({
5609
5865
  };
5610
5866
  }
5611
5867
  }
5612
- var FIRST_VERSION_TIMEOUT_MS, RETRY_VERSION_TIMEOUT_MS;
5868
+ var FIRST_VERSION_TIMEOUT_MS, RETRY_VERSION_TIMEOUT_MS, AUTH_PROBE_BUDGET_MS, MIN_SKILL_PROBE_MS;
5613
5869
  var init_claude_auth_check = __esm({
5614
5870
  "../../scripts/virtual-office/code-runner/claude-auth-check.mjs"() {
5615
5871
  "use strict";
@@ -5617,8 +5873,11 @@ var init_claude_auth_check = __esm({
5617
5873
  init_agent_auth_tier();
5618
5874
  init_cli_version_floor();
5619
5875
  init_windows_claude_launch();
5876
+ init_claude_skill_capability();
5620
5877
  FIRST_VERSION_TIMEOUT_MS = 4500;
5621
5878
  RETRY_VERSION_TIMEOUT_MS = 2e3;
5879
+ AUTH_PROBE_BUDGET_MS = 9500;
5880
+ MIN_SKILL_PROBE_MS = 250;
5622
5881
  }
5623
5882
  });
5624
5883
 
@@ -5639,6 +5898,7 @@ function runAgentTask({
5639
5898
  maxBudgetUsd = null,
5640
5899
  researchHarness = false,
5641
5900
  toolPolicy = "default",
5901
+ structuredOutputSchema,
5642
5902
  env: env2 = process.env,
5643
5903
  onProgress = () => {
5644
5904
  },
@@ -5653,11 +5913,14 @@ function runAgentTask({
5653
5913
  exitDrainGraceMs = 300,
5654
5914
  armTerminalCleanup = armTerminalProcessCleanup,
5655
5915
  spawnImpl = spawn2,
5656
- sandbox = null
5916
+ sandbox = null,
5917
+ allowApiBilling = false
5657
5918
  }) {
5658
5919
  return new Promise((resolve3) => {
5659
- const args = runner.buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, prompt });
5660
- const spawnEnv = typeof runner.applyAuthEnv === "function" ? runner.applyAuthEnv(env2) : env2;
5920
+ const args = runner.buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema, prompt });
5921
+ const authedEnv = typeof runner.applyAuthEnv === "function" ? runner.applyAuthEnv(env2) : env2;
5922
+ const billing = applyApiBillingPolicy(authedEnv, { runnerEnv: env2, allowApiBilling });
5923
+ const spawnEnv = billing.env;
5661
5924
  const costBasis = typeof runner.costBasis === "function" ? runner.costBasis(spawnEnv) : "unknown";
5662
5925
  if (costBasis === "vendor_billed" && runner.enforcesBudgetCap !== true && env2.VO_CODE_RUNNER_ALLOW_UNCAPPED_VENDOR_BILLED !== "1") {
5663
5926
  throw new Error(
@@ -5701,7 +5964,7 @@ function runAgentTask({
5701
5964
  } catch {
5702
5965
  }
5703
5966
  let buffer = "";
5704
- let result = { ok: false, costUsd: null, costBasis, summary: "", lastAgentMessage: null, numTurns: null, tokenUsage: null, modelUsage: null, executionStarted: false, killed: false };
5967
+ let result = { ok: false, costUsd: null, costBasis, summary: "", lastAgentMessage: null, structuredOutput: null, terminalSubtype: null, numTurns: null, tokenUsage: null, modelUsage: null, executionStarted: false, killed: false, agentAuthSource: billing.agent_auth_source };
5705
5968
  child.once("spawn", () => {
5706
5969
  result = { ...result, executionStarted: true };
5707
5970
  Promise.resolve(onSpawn()).catch(() => {
@@ -5739,6 +6002,8 @@ function runAgentTask({
5739
6002
  // A budget/turn-capped run's honest last message, kept OUT of summary (see
5740
6003
  // claude-result-event.cappedRunLastMessage) and surfaced in the PR body.
5741
6004
  lastAgentMessage: cappedRunLastMessage(evt, lastProgress),
6005
+ structuredOutput: Object.hasOwn(evt, "structuredOutput") ? evt.structuredOutput : result.structuredOutput,
6006
+ terminalSubtype: Object.hasOwn(evt, "terminalSubtype") ? evt.terminalSubtype : result.terminalSubtype,
5742
6007
  numTurns: evt.numTurns,
5743
6008
  // MUST be listed explicitly. This assignment spreads the PREVIOUS
5744
6009
  // result and then names each field it carries forward, so anything
@@ -5820,7 +6085,9 @@ function runAgentTask({
5820
6085
  hardKill();
5821
6086
  return;
5822
6087
  }
5823
- if (decision.delayMs != null) wallTimer = setTimeout(armDeadline, decision.delayMs);
6088
+ if (decision.delayMs !== null && decision.delayMs !== void 0) {
6089
+ wallTimer = setTimeout(armDeadline, decision.delayMs);
6090
+ }
5824
6091
  };
5825
6092
  armDeadline();
5826
6093
  child.stdout.on("data", (chunk) => {
@@ -5881,6 +6148,8 @@ var init_claude_runner = __esm({
5881
6148
  init_claude_stream_event();
5882
6149
  init_claude_result_event();
5883
6150
  init_claude_auth_check();
6151
+ init_claude_skill_capability();
6152
+ init_agent_auth_attestation();
5884
6153
  ClaudeRunner = class {
5885
6154
  get enforcesBudgetCap() {
5886
6155
  return true;
@@ -5888,8 +6157,8 @@ var init_claude_runner = __esm({
5888
6157
  get binary() {
5889
6158
  return "claude";
5890
6159
  }
5891
- buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy } = {}) {
5892
- return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy });
6160
+ buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema } = {}) {
6161
+ return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema });
5893
6162
  }
5894
6163
  parseEvent(line) {
5895
6164
  return parseStreamEvent(line);
@@ -5913,6 +6182,9 @@ var init_claude_runner = __esm({
5913
6182
  async checkAuth() {
5914
6183
  return checkClaudeAuth();
5915
6184
  }
6185
+ checkSkillCapability({ bin = this.binary, env: env2 = process.env } = {}) {
6186
+ return probeClaudeSkillCapability({ bin, env: env2, freshIdentity: true });
6187
+ }
5916
6188
  };
5917
6189
  claudeRunner = new ClaudeRunner();
5918
6190
  }
@@ -6096,10 +6368,10 @@ var init_error_message = __esm({
6096
6368
  });
6097
6369
 
6098
6370
  // ../../scripts/virtual-office/code-runner/codex-runner.mjs
6099
- import { spawnSync as spawnSync6 } from "node:child_process";
6371
+ import { spawnSync as spawnSync7 } from "node:child_process";
6100
6372
  import { existsSync as existsSync10 } from "node:fs";
6101
6373
  import { win32 as win322 } from "node:path";
6102
- function isTruthyFlag2(value) {
6374
+ function isTruthyFlag3(value) {
6103
6375
  return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
6104
6376
  }
6105
6377
  function resolveCodexBinary({
@@ -6211,7 +6483,7 @@ var init_codex_runner = __esm({
6211
6483
  CODEX_PREFER_LOGIN_ENV = "VO_RUNNER_CODEX_PREFER_LOGIN";
6212
6484
  LEGACY_PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
6213
6485
  CodexRunner = class {
6214
- constructor({ spawn: spawn5 = spawnSync6, resolveBinary = resolveCodexBinary, env: env2 = process.env } = {}) {
6486
+ constructor({ spawn: spawn5 = spawnSync7, resolveBinary = resolveCodexBinary, env: env2 = process.env } = {}) {
6215
6487
  this.spawn = spawn5;
6216
6488
  this.resolveBinary = resolveBinary;
6217
6489
  this.env = env2;
@@ -6250,7 +6522,7 @@ var init_codex_runner = __esm({
6250
6522
  };
6251
6523
  }
6252
6524
  applyAuthEnv(env2 = process.env) {
6253
- if (isTruthyFlag2(env2[CODEX_PREFER_LOGIN_ENV]) || isTruthyFlag2(env2[LEGACY_PREFER_LOGIN_ENV])) {
6525
+ if (isTruthyFlag3(env2[CODEX_PREFER_LOGIN_ENV]) || isTruthyFlag3(env2[LEGACY_PREFER_LOGIN_ENV])) {
6254
6526
  const out = { ...env2 };
6255
6527
  delete out.OPENAI_API_KEY;
6256
6528
  delete out.CODEX_API_KEY;
@@ -6324,7 +6596,7 @@ ${login.stderr || ""}`.trim();
6324
6596
  });
6325
6597
 
6326
6598
  // ../../scripts/virtual-office/code-runner/cursor-runner.mjs
6327
- import { spawnSync as spawnSync7 } from "node:child_process";
6599
+ import { spawnSync as spawnSync8 } from "node:child_process";
6328
6600
  function buildCursorArgs({ model, prompt } = {}) {
6329
6601
  const args = ["-p", "--output-format", "stream-json", "--force"];
6330
6602
  if (model) {
@@ -6436,7 +6708,7 @@ var init_cursor_runner = __esm({
6436
6708
  /** Best-effort: is `cursor-agent` on PATH? Never throws. */
6437
6709
  async checkAuth() {
6438
6710
  try {
6439
- const { status, error } = spawnSync7("cursor-agent", ["--version"], {
6711
+ const { status, error } = spawnSync8("cursor-agent", ["--version"], {
6440
6712
  shell: false,
6441
6713
  windowsHide: true,
6442
6714
  timeout: 3e3,
@@ -7155,9 +7427,9 @@ var init_rate_limit_detector_core = __esm({
7155
7427
 
7156
7428
  // ../../scripts/virtual-office/code-runner/rate-limit-resume-state.mjs
7157
7429
  import fsp10 from "node:fs/promises";
7158
- import path14 from "node:path";
7430
+ import path15 from "node:path";
7159
7431
  async function atomicWrite(file, content) {
7160
- await fsp10.mkdir(path14.dirname(file), { recursive: true });
7432
+ await fsp10.mkdir(path15.dirname(file), { recursive: true });
7161
7433
  const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
7162
7434
  const handle = await fsp10.open(temp, "wx");
7163
7435
  try {
@@ -7218,7 +7490,7 @@ function writeResumeAttempts(file, store) {
7218
7490
  }
7219
7491
  async function acquireLock(lockFile, { now = Date.now, sleep: sleep3 = delay } = {}) {
7220
7492
  const deadline = now() + LOCK_WAIT_MS;
7221
- await fsp10.mkdir(path14.dirname(lockFile), { recursive: true });
7493
+ await fsp10.mkdir(path15.dirname(lockFile), { recursive: true });
7222
7494
  for (; ; ) {
7223
7495
  let handle;
7224
7496
  try {
@@ -7522,7 +7794,7 @@ var init_auto_merge = __esm({
7522
7794
  });
7523
7795
 
7524
7796
  // ../../scripts/virtual-office/code-runner/pr-overlap-gate.mjs
7525
- import { spawnSync as spawnSync8 } from "node:child_process";
7797
+ import { spawnSync as spawnSync9 } from "node:child_process";
7526
7798
  import { existsSync as existsSync11 } from "node:fs";
7527
7799
  import { dirname as dirname6, join as join9 } from "node:path";
7528
7800
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -7688,14 +7960,14 @@ function parsePorcelainZ(out) {
7688
7960
  for (let i = 0; i < tokens.length; i += 1) {
7689
7961
  const token2 = tokens[i];
7690
7962
  if (!token2) continue;
7691
- const path23 = token2.slice(3);
7692
- if (path23) files.push(path23);
7963
+ const path24 = token2.slice(3);
7964
+ if (path24) files.push(path24);
7693
7965
  if (token2[0] === "R" || token2[0] === "C") i += 1;
7694
7966
  }
7695
7967
  return files;
7696
7968
  }
7697
- function isAgentScratch(path23) {
7698
- const normalized = String(path23 || "");
7969
+ function isAgentScratch(path24) {
7970
+ const normalized = String(path24 || "");
7699
7971
  return SCRATCH_PATTERNS.some((pattern) => pattern.test(normalized));
7700
7972
  }
7701
7973
  var SCRATCH_PATTERNS;
@@ -7713,7 +7985,7 @@ var init_publish_file_state = __esm({
7713
7985
  });
7714
7986
 
7715
7987
  // ../../scripts/virtual-office/code-runner/publish.mjs
7716
- import { spawnSync as spawnSync9 } from "node:child_process";
7988
+ import { spawnSync as spawnSync10 } from "node:child_process";
7717
7989
  function isMaxTurnsResult(summary) {
7718
7990
  return /(^|[^a-z])error[-_ ]?max[-_ ]?turns([^a-z]|$)|max[-_ ]?turns/i.test(String(summary || ""));
7719
7991
  }
@@ -7952,7 +8224,7 @@ var init_executor = __esm({
7952
8224
 
7953
8225
  // ../../scripts/virtual-office/code-runner/test-gen-gate.mjs
7954
8226
  import fs7 from "node:fs";
7955
- import path15 from "node:path";
8227
+ import path16 from "node:path";
7956
8228
  async function postFailed(client, id, message, result) {
7957
8229
  try {
7958
8230
  await client.postProgress(id, {
@@ -7988,7 +8260,7 @@ async function gateTestGenTaskOrFail({ client, id, task, files, worktreeDir, env
7988
8260
  }
7989
8261
  let testSource = "";
7990
8262
  try {
7991
- testSource = fs7.readFileSync(path15.join(worktreeDir, testFile), "utf8");
8263
+ testSource = fs7.readFileSync(path16.join(worktreeDir, testFile), "utf8");
7992
8264
  } catch (err) {
7993
8265
  await postFailed(client, id, `could not read generated test ${testFile}: ${err.message}`, "gate_test_unreadable");
7994
8266
  return true;
@@ -8038,7 +8310,7 @@ var init_test_gen_gate = __esm({
8038
8310
  // ../../scripts/virtual-office/code-runner/completion-gate.mjs
8039
8311
  import { execFile } from "node:child_process";
8040
8312
  import fs8 from "node:fs";
8041
- import path16 from "node:path";
8313
+ import path17 from "node:path";
8042
8314
  function resolveCompletionGate(task) {
8043
8315
  const raw = task?.completion_gate;
8044
8316
  if (raw === void 0 || raw === null) return null;
@@ -8076,14 +8348,14 @@ function workspaceFingerprint(worktreeDir, execFileImpl = execFile) {
8076
8348
  }
8077
8349
  function readState(worktreeDir) {
8078
8350
  try {
8079
- return JSON.parse(fs8.readFileSync(path16.join(worktreeDir, COMPLETION_GATE_STATE_FILE), "utf8"));
8351
+ return JSON.parse(fs8.readFileSync(path17.join(worktreeDir, COMPLETION_GATE_STATE_FILE), "utf8"));
8080
8352
  } catch {
8081
8353
  return null;
8082
8354
  }
8083
8355
  }
8084
8356
  function writeState(worktreeDir, state) {
8085
8357
  try {
8086
- fs8.writeFileSync(path16.join(worktreeDir, COMPLETION_GATE_STATE_FILE), `${JSON.stringify(state)}
8358
+ fs8.writeFileSync(path17.join(worktreeDir, COMPLETION_GATE_STATE_FILE), `${JSON.stringify(state)}
8087
8359
  `, "utf8");
8088
8360
  } catch {
8089
8361
  }
@@ -9221,7 +9493,7 @@ var init_headless_execution_contract = __esm({
9221
9493
  });
9222
9494
 
9223
9495
  // ../../scripts/virtual-office/code-runner/skill-catalog.mjs
9224
- import { readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
9496
+ import { readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync5 } from "node:fs";
9225
9497
  import { dirname as dirname7, join as join10 } from "node:path";
9226
9498
  import { fileURLToPath as fileURLToPath4 } from "node:url";
9227
9499
  function parseFrontmatterNameDescription(raw) {
@@ -9243,12 +9515,12 @@ function parseFrontmatterNameDescription(raw) {
9243
9515
  }
9244
9516
  function isRepoCheckout(dir) {
9245
9517
  try {
9246
- if (!statSync4(join10(dir, ".claude", "skills")).isDirectory()) return false;
9518
+ if (!statSync5(join10(dir, ".claude", "skills")).isDirectory()) return false;
9247
9519
  } catch {
9248
9520
  return false;
9249
9521
  }
9250
9522
  try {
9251
- statSync4(join10(dir, ".git"));
9523
+ statSync5(join10(dir, ".git"));
9252
9524
  return true;
9253
9525
  } catch {
9254
9526
  return false;
@@ -9274,7 +9546,7 @@ function loadSkillCatalog({ repoRoot: repoRoot2 = resolveDefaultRepoRoot() } = {
9274
9546
  for (const entry of readdirSync3(skillsDir)) {
9275
9547
  const dir = join10(skillsDir, entry);
9276
9548
  try {
9277
- if (!statSync4(dir).isDirectory()) continue;
9549
+ if (!statSync5(dir).isDirectory()) continue;
9278
9550
  const parsed = parseFrontmatterNameDescription(
9279
9551
  readFileSync8(join10(dir, "SKILL.md"), "utf8")
9280
9552
  );
@@ -9804,7 +10076,7 @@ var init_task_prompt = __esm({
9804
10076
  import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
9805
10077
  import { chmod, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
9806
10078
  import os2 from "node:os";
9807
- import path17 from "node:path";
10079
+ import path18 from "node:path";
9808
10080
  function safeTaskToken(taskId) {
9809
10081
  return String(taskId || "task").replace(/[^0-9A-Za-z_-]/gu, "_").slice(0, 48) || "task";
9810
10082
  }
@@ -9817,9 +10089,9 @@ function hasGeneratedPrefix(name) {
9817
10089
  return name.startsWith(DIRECTORY_PREFIX) || name.startsWith(LEGACY_DIRECTORY_PREFIX);
9818
10090
  }
9819
10091
  function assertGeneratedDirectory(directory, containmentRoot) {
9820
- const resolvedDirectory = path17.resolve(directory);
9821
- const resolvedRoot = path17.resolve(containmentRoot);
9822
- if (path17.dirname(resolvedDirectory) !== resolvedRoot || !hasGeneratedPrefix(path17.basename(resolvedDirectory))) {
10092
+ const resolvedDirectory = path18.resolve(directory);
10093
+ const resolvedRoot = path18.resolve(containmentRoot);
10094
+ if (path18.dirname(resolvedDirectory) !== resolvedRoot || !hasGeneratedPrefix(path18.basename(resolvedDirectory))) {
9823
10095
  throw new Error("refusing to clean an unverified task-attachment directory");
9824
10096
  }
9825
10097
  return resolvedDirectory;
@@ -9828,7 +10100,7 @@ async function resolveContainmentRoot(worktreeDir) {
9828
10100
  if (typeof worktreeDir !== "string" || !worktreeDir.trim()) {
9829
10101
  throw new Error("refusing to materialize task attachments outside an agent-readable worktree: no worktreeDir given");
9830
10102
  }
9831
- const root = path17.resolve(worktreeDir);
10103
+ const root = path18.resolve(worktreeDir);
9832
10104
  const stats = await stat(root).catch(() => null);
9833
10105
  if (!stats?.isDirectory()) {
9834
10106
  throw new Error(`refusing to materialize task attachments: agent worktree root is not a directory (${root})`);
@@ -9837,15 +10109,15 @@ async function resolveContainmentRoot(worktreeDir) {
9837
10109
  }
9838
10110
  async function createAttachmentDirectory(taskId, containmentRoot) {
9839
10111
  const root = await resolveContainmentRoot(containmentRoot);
9840
- const directory = await mkdtemp(path17.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
10112
+ const directory = await mkdtemp(path18.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
9841
10113
  const [realRoot, realDirectory] = await Promise.all([realpath(root), realpath(directory)]);
9842
- if (path17.dirname(realDirectory) !== realRoot) {
10114
+ if (path18.dirname(realDirectory) !== realRoot) {
9843
10115
  await rm(directory, { recursive: true, force: true }).catch(() => void 0);
9844
10116
  throw new Error("task-attachment directory escaped the agent worktree root");
9845
10117
  }
9846
- await writeFile(path17.join(directory, GITIGNORE_FILE), GITIGNORE_BODY, { encoding: "utf8", mode: 384 });
9847
- const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID4(), directory: path17.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
9848
- await writeFile(path17.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
10118
+ await writeFile(path18.join(directory, GITIGNORE_FILE), GITIGNORE_BODY, { encoding: "utf8", mode: 384 });
10119
+ const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID4(), directory: path18.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
10120
+ await writeFile(path18.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
9849
10121
  return { directory, marker, root, cleaned: false };
9850
10122
  }
9851
10123
  async function cleanupGeneratedDirectory(state) {
@@ -9859,7 +10131,7 @@ async function cleanupGeneratedDirectory(state) {
9859
10131
  state.cleaned = true;
9860
10132
  return;
9861
10133
  }
9862
- const marker = await readFile(path17.join(directory, MARKER_FILE), "utf8").catch(() => "");
10134
+ const marker = await readFile(path18.join(directory, MARKER_FILE), "utf8").catch(() => "");
9863
10135
  if (marker !== state.marker) throw new Error("refusing to clean a task-attachment directory without its exact marker");
9864
10136
  await rm(directory, { recursive: true, force: true });
9865
10137
  state.cleaned = true;
@@ -9878,7 +10150,7 @@ async function sweepStaleTaskAttachmentDirectories({
9878
10150
  now = Date.now(),
9879
10151
  maxAgeMs = DEFAULT_STALE_AGE_MS
9880
10152
  } = {}) {
9881
- const root = path17.resolve(tempRoot);
10153
+ const root = path18.resolve(tempRoot);
9882
10154
  if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) throw new Error("stale attachment age must be positive");
9883
10155
  const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
9884
10156
  if (error?.code === "ENOENT") return [];
@@ -9887,8 +10159,8 @@ async function sweepStaleTaskAttachmentDirectories({
9887
10159
  let removed = 0;
9888
10160
  for (const entry of entries) {
9889
10161
  if (!entry.isDirectory() || !hasGeneratedPrefix(entry.name)) continue;
9890
- const directory = assertGeneratedDirectory(path17.join(root, entry.name), root);
9891
- const markerRaw = await readFile(path17.join(directory, MARKER_FILE), "utf8").catch(() => "");
10162
+ const directory = assertGeneratedDirectory(path18.join(root, entry.name), root);
10163
+ const markerRaw = await readFile(path18.join(directory, MARKER_FILE), "utf8").catch(() => "");
9892
10164
  const marker = parseOwnedMarker(markerRaw, entry.name);
9893
10165
  if (!marker) continue;
9894
10166
  const directoryStat = await stat(directory);
@@ -9945,8 +10217,8 @@ async function materializeTaskAttachments(client, task, { worktreeDir } = {}) {
9945
10217
  const sha2562 = createHash5("sha256").update(content).digest("hex");
9946
10218
  if (sha2562 !== ref.sha256) throw new Error(`attachment ${ref.attachment_id} sha256 mismatch`);
9947
10219
  const name = sanitizeTaskAttachmentName(ref.name, index);
9948
- const filePath = path17.resolve(state.directory, name);
9949
- if (path17.dirname(filePath) !== state.directory) throw new Error(`attachment ${ref.attachment_id} resolved outside its task directory`);
10220
+ const filePath = path18.resolve(state.directory, name);
10221
+ if (path18.dirname(filePath) !== state.directory) throw new Error(`attachment ${ref.attachment_id} resolved outside its task directory`);
9950
10222
  await writeFile(filePath, content, { flag: "wx", mode: 384 });
9951
10223
  await chmod(filePath, 384);
9952
10224
  files.push({ attachmentId: ref.attachment_id, name, mime: ref.mime, sizeBytes: ref.size_bytes, sha256: sha2562, path: filePath });
@@ -10031,9 +10303,9 @@ async function readSpool(spoolDir = SPOOL_DIR) {
10031
10303
  }
10032
10304
  return out;
10033
10305
  }
10034
- async function readCloudMap(path23) {
10306
+ async function readCloudMap(path24) {
10035
10307
  try {
10036
- return JSON.parse(await readFile2(path23, "utf8"));
10308
+ return JSON.parse(await readFile2(path24, "utf8"));
10037
10309
  } catch {
10038
10310
  return {};
10039
10311
  }
@@ -10334,9 +10606,9 @@ function backoffMs(streak, baseMs) {
10334
10606
  if (streak <= 0) return 0;
10335
10607
  return Math.min(baseMs * 2 ** Math.min(streak - 1, 20), MAX_BACKOFF_MS);
10336
10608
  }
10337
- async function loadState(path23) {
10609
+ async function loadState(path24) {
10338
10610
  try {
10339
- const parsed = JSON.parse(await readFile3(path23, "utf8"));
10611
+ const parsed = JSON.parse(await readFile3(path24, "utf8"));
10340
10612
  if (parsed && typeof parsed === "object" && Number.isInteger(parsed.byte_offset) && parsed.byte_offset >= 0) {
10341
10613
  return { ...parsed, byte_offset: parsed.byte_offset };
10342
10614
  }
@@ -10344,15 +10616,15 @@ async function loadState(path23) {
10344
10616
  }
10345
10617
  return { byte_offset: 0, last_event_id: null, forwarded_total: 0, rejected_total: 0, rejected_event_ids: [] };
10346
10618
  }
10347
- async function saveState(path23, state) {
10348
- await mkdir2(dirname9(path23), { recursive: true });
10349
- await writeFile3(path23, JSON.stringify(state, null, 2), "utf8");
10619
+ async function saveState(path24, state) {
10620
+ await mkdir2(dirname9(path24), { recursive: true });
10621
+ await writeFile3(path24, JSON.stringify(state, null, 2), "utf8");
10350
10622
  }
10351
- async function readNewBytes(path23, offset, max) {
10352
- const st = await stat2(path23);
10623
+ async function readNewBytes(path24, offset, max) {
10624
+ const st = await stat2(path24);
10353
10625
  if (st.size <= offset) return { buf: Buffer.alloc(0), size: st.size };
10354
10626
  const length = Math.min(st.size - offset, max);
10355
- const fh = await open(path23, "r");
10627
+ const fh = await open(path24, "r");
10356
10628
  try {
10357
10629
  const buf = Buffer.alloc(length);
10358
10630
  const { bytesRead } = await fh.read(buf, 0, length, offset);
@@ -10603,6 +10875,10 @@ var init_telemetry_forwarder = __esm({
10603
10875
  });
10604
10876
 
10605
10877
  // ../../scripts/virtual-office/code-runner/loop-ticks.mjs
10878
+ function supportedTaskKindsFor(availableAgents) {
10879
+ const claude = Array.isArray(availableAgents) ? availableAgents.find((row) => row?.agent === "claude") : null;
10880
+ return claude?.installed === true && claude.authenticated === true && claude.skill_capable === true ? RUNNER_SUPPORTED_TASK_KINDS : RUNNER_SUPPORTED_TASK_KINDS.filter((kind) => kind !== "skill");
10881
+ }
10606
10882
  function makeLoopTicks({
10607
10883
  client,
10608
10884
  cfg,
@@ -10636,6 +10912,16 @@ function makeLoopTicks({
10636
10912
  getAgentAvailability = () => null,
10637
10913
  // Cached account-usage provider (account-usage.mjs); [] omits the field.
10638
10914
  getAccountUsage = () => [],
10915
+ // Host-preflight verdict (host-preflight.mjs) — why this host is or is not
10916
+ // claiming. GATED OFF BY DEFAULT and that is deliberate: the plane's
10917
+ // heartbeat input schema is `.strict()`, so sending `host_health` before
10918
+ // that schema accepts it would 400 EVERY beat and take the machine off the
10919
+ // fleet — the 2026-07-25 `version` outage, and it would fire hardest on
10920
+ // exactly the broken hosts this field describes. Flip
10921
+ // VO_HOST_HEALTH_HEARTBEAT=1 once runner-host-health-v1 is wired into
10922
+ // runner-heartbeat-v1.ts and deployed. The claim GATE does not depend on
10923
+ // this: a blocked host stops claiming either way.
10924
+ getHostHealth = () => null,
10639
10925
  // Injectable for tests; default to the real scheduler + wall clock.
10640
10926
  runResumeScheduler = runScheduler,
10641
10927
  now: nowFn = () => Date.now(),
@@ -10736,10 +11022,17 @@ function makeLoopTicks({
10736
11022
  ...servedOperators.length > 0 ? { servedOperators } : {},
10737
11023
  ...Array.isArray(availableAgents) && availableAgents.length > 0 ? { availableAgents } : {},
10738
11024
  ...Array.isArray(accountUsage) && accountUsage.length > 0 ? { accountUsage } : {},
11025
+ ...env2.VO_HOST_HEALTH_HEARTBEAT === "1" && getHostHealth() ? { hostHealth: getHostHealth() } : {},
10739
11026
  uptimeSec: Math.floor(process.uptime()),
10740
11027
  activeTasks: getActive(),
10741
11028
  maxConcurrency: cfg.maxConcurrency,
10742
- supportedTaskKinds: RUNNER_SUPPORTED_TASK_KINDS,
11029
+ supportedTaskKinds: supportedTaskKindsFor(availableAgents),
11030
+ // Attested EVERY beat, from the daemon's own env + keychain, so the plane's
11031
+ // runner-api-billing-gate can refuse a metered machine BEFORE it claims.
11032
+ // Never throws (see resolveRunnerAttestedAuthSource) — a heartbeat that
11033
+ // 500s on its own attestation would take the host off the fleet, which is
11034
+ // strictly worse than the spend it was trying to prevent.
11035
+ agentAuthSource: resolveRunnerAttestedAuthSource(env2),
10743
11036
  ...capacityFields,
10744
11037
  ...localModelFields,
10745
11038
  ...preparedJobFields
@@ -10771,6 +11064,7 @@ var init_loop_ticks = __esm({
10771
11064
  init_session_spool_forwarder();
10772
11065
  init_rate_limit_resume_scheduler();
10773
11066
  init_telemetry_forwarder();
11067
+ init_agent_auth_attestation();
10774
11068
  HEARTBEAT_MS = 6e4;
10775
11069
  DEFAULT_RESUME_SCHEDULE_SEC = 300;
10776
11070
  RUNNER_SUPPORTED_TASK_KINDS = Object.freeze(["code", "inference", "skill"]);
@@ -10899,7 +11193,7 @@ function resolveAgentClaimContext(provider, defaultAgent) {
10899
11193
  async function collectAgentAvailability({
10900
11194
  agents = listAgents(),
10901
11195
  runnerFor,
10902
- probeTimeoutMs = PROBE_TIMEOUT_MS
11196
+ probeTimeoutMs = PROBE_TIMEOUT_MS2
10903
11197
  } = {}) {
10904
11198
  const probes = agents.map(async (agent) => {
10905
11199
  const degraded = { agent, installed: false, authenticated: false };
@@ -10917,6 +11211,7 @@ async function collectAgentAvailability({
10917
11211
  installed,
10918
11212
  authenticated,
10919
11213
  ...typeof r?.version === "string" && r.version ? { version: r.version } : {},
11214
+ ...typeof r?.skillCapable === "boolean" ? { skill_capable: r.skillCapable } : {},
10920
11215
  // Omitted when unknown, which is what an older daemon's silence already
10921
11216
  // means — the control-plane schema resolves BOTH to 'unknown'. Never
10922
11217
  // invent a tier to fill the gap.
@@ -10977,7 +11272,7 @@ function makeAgentAvailabilityProvider({
10977
11272
  }
10978
11273
  };
10979
11274
  }
10980
- var DEFAULT_TTL_MS, PROBE_TIMEOUT_MS;
11275
+ var DEFAULT_TTL_MS, PROBE_TIMEOUT_MS2;
10981
11276
  var init_agent_availability = __esm({
10982
11277
  "../../scripts/virtual-office/code-runner/agent-availability.mjs"() {
10983
11278
  "use strict";
@@ -10985,7 +11280,7 @@ var init_agent_availability = __esm({
10985
11280
  init_agent_auth_probe_process();
10986
11281
  init_agent_auth_tier();
10987
11282
  DEFAULT_TTL_MS = 5 * 60 * 1e3;
10988
- PROBE_TIMEOUT_MS = 1e4;
11283
+ PROBE_TIMEOUT_MS2 = 1e4;
10989
11284
  }
10990
11285
  });
10991
11286
 
@@ -11447,10 +11742,10 @@ function formatShadowLogLine(record) {
11447
11742
  const loud = record.unexplained_fields?.length ? "!! " : "";
11448
11743
  return `${loud}[prepared-job-shadow] ${parts.join(" ")}`;
11449
11744
  }
11450
- function appendShadowRecord(record, { path: path23 = PREPARED_JOB_SHADOW_SINK, append = appendFileSync, mkdir: mkdir5 = mkdirSync8 } = {}) {
11745
+ function appendShadowRecord(record, { path: path24 = PREPARED_JOB_SHADOW_SINK, append = appendFileSync, mkdir: mkdir5 = mkdirSync8 } = {}) {
11451
11746
  try {
11452
- mkdir5(dirname10(path23), { recursive: true });
11453
- append(path23, `${JSON.stringify(record)}
11747
+ mkdir5(dirname10(path24), { recursive: true });
11748
+ append(path24, `${JSON.stringify(record)}
11454
11749
  `, "utf8");
11455
11750
  return true;
11456
11751
  } catch {
@@ -11770,7 +12065,7 @@ var init_shared = __esm({
11770
12065
  // ../../scripts/virtual-office/code-runner/account-usage/claude.mjs
11771
12066
  import fs10 from "node:fs";
11772
12067
  import os3 from "node:os";
11773
- import path18 from "node:path";
12068
+ import path19 from "node:path";
11774
12069
  function fileCaptureTime(filePath, explicit, statFn) {
11775
12070
  if (typeof explicit === "string" && explicit) return explicit;
11776
12071
  try {
@@ -11784,7 +12079,7 @@ function usageBaseUrl(env2 = process.env) {
11784
12079
  return String(raw).replace(/\/+$/, "");
11785
12080
  }
11786
12081
  function readOAuthToken({ homeDir = os3.homedir(), read = readJson2, now = Date.now() } = {}) {
11787
- const creds = read(path18.join(homeDir, ".claude", ".credentials.json"));
12082
+ const creds = read(path19.join(homeDir, ".claude", ".credentials.json"));
11788
12083
  const oauth = creds && typeof creds === "object" ? creds.claudeAiOauth : null;
11789
12084
  if (!oauth || typeof oauth !== "object") return null;
11790
12085
  const token2 = typeof oauth.accessToken === "string" ? oauth.accessToken.trim() : "";
@@ -11794,7 +12089,7 @@ function readOAuthToken({ homeDir = os3.homedir(), read = readJson2, now = Date.
11794
12089
  return token2;
11795
12090
  }
11796
12091
  function readAccountId({ homeDir = os3.homedir(), read = readJson2 } = {}) {
11797
- const cfg = read(path18.join(homeDir, ".claude.json"));
12092
+ const cfg = read(path19.join(homeDir, ".claude.json"));
11798
12093
  const account = cfg && typeof cfg === "object" ? cfg.oauthAccount : null;
11799
12094
  return account && typeof account.accountUuid === "string" ? account.accountUuid : null;
11800
12095
  }
@@ -11904,7 +12199,7 @@ function readClaudeFileUsage({
11904
12199
  if (age === null || age > MAX_FILE_AGE_MS) return null;
11905
12200
  return row;
11906
12201
  };
11907
- const statusPath = path18.join(homeDir, ".claude", "claude-usage.json");
12202
+ const statusPath = path19.join(homeDir, ".claude", "claude-usage.json");
11908
12203
  const status = read(statusPath);
11909
12204
  if (status && (status.seven_day || status.five_hour)) {
11910
12205
  const row = fresh(makeUsageRow({
@@ -11919,7 +12214,7 @@ function readClaudeFileUsage({
11919
12214
  }));
11920
12215
  if (row) return row;
11921
12216
  }
11922
- const weeklyPath = path18.join(homeDir, ".claude", "claude-weekly-usage.json");
12217
+ const weeklyPath = path19.join(homeDir, ".claude", "claude-weekly-usage.json");
11923
12218
  const weekly = read(weeklyPath);
11924
12219
  if (weekly) {
11925
12220
  const row = fresh(makeUsageRow({
@@ -12733,11 +13028,11 @@ var init_watcher_adoption = __esm({
12733
13028
  // ../../scripts/virtual-office/code-runner/watcher-github-token.mjs
12734
13029
  function makeWatcherTokenProvider(client, { required = true, allowAmbient = false, now = () => Date.now(), log: log2 = () => {
12735
13030
  } } = {}) {
12736
- const cache = /* @__PURE__ */ new Map();
13031
+ const cache2 = /* @__PURE__ */ new Map();
12737
13032
  let ciUnreadableLoggedAt = null;
12738
13033
  return async (repo) => {
12739
13034
  const key = String(repo).toLowerCase();
12740
- const prior = cache.get(key);
13035
+ const prior = cache2.get(key);
12741
13036
  if (prior && now() - prior.at < 45 * 60 * 1e3) return prior.token;
12742
13037
  const result = await client.getInstallationToken({ required, readOnly: true, repo });
12743
13038
  if (!result?.token) {
@@ -12748,7 +13043,7 @@ function makeWatcherTokenProvider(client, { required = true, allowAmbient = fals
12748
13043
  ciUnreadableLoggedAt = now();
12749
13044
  log2(`watch: the plane minted a read token for ${repo} WITHOUT CI read (ci_readable=false \u2014 the GitHub App installation has not accepted checks:read/statuses:read); PR CI stays unreadable until the operator accepts the App permission update`);
12750
13045
  }
12751
- cache.set(key, { token: result.token, at: now() });
13046
+ cache2.set(key, { token: result.token, at: now() });
12752
13047
  return result.token;
12753
13048
  };
12754
13049
  }
@@ -12914,7 +13209,7 @@ function noteCiViaRest(log2) {
12914
13209
  log2("watch: CI status read via REST check-runs/status (gh's GraphQL rollup needs actions:read for checkSuite.workflowRun, which the read scope does not carry)");
12915
13210
  }
12916
13211
  async function readCommitCiViaRest(repo, sha, { run, env: env2 }) {
12917
- const api = async (path23) => JSON.parse(await run("gh", ["api", path23], { timeout: 3e4, env: env2 }) || "{}");
13212
+ const api = async (path24) => JSON.parse(await run("gh", ["api", path24], { timeout: 3e4, env: env2 }) || "{}");
12918
13213
  const rollup = [];
12919
13214
  let total = null;
12920
13215
  for (let page = 1; page <= REST_MAX_PAGES && (total === null || rollup.length < total); page += 1) {
@@ -13578,9 +13873,9 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
13578
13873
  res.end();
13579
13874
  return;
13580
13875
  }
13581
- const path23 = String(req.url || "").split("?")[0];
13876
+ const path24 = String(req.url || "").split("?")[0];
13582
13877
  res.setHeader("content-type", "application/json");
13583
- if (req.method === "GET" && path23 === "/status") {
13878
+ if (req.method === "GET" && path24 === "/status") {
13584
13879
  let status;
13585
13880
  try {
13586
13881
  status = getStatus();
@@ -13591,7 +13886,7 @@ function buildControlHandler({ getStatus, requestStop, allowedOrigin }) {
13591
13886
  res.end(JSON.stringify({ ok: true, ...status }));
13592
13887
  return;
13593
13888
  }
13594
- if (req.method === "POST" && path23 === "/stop") {
13889
+ if (req.method === "POST" && path24 === "/stop") {
13595
13890
  if (!isControlOriginAllowed(req.headers.origin, allowedOrigin) || !req.headers["x-vo-control"]) {
13596
13891
  res.statusCode = 403;
13597
13892
  res.end(JSON.stringify({ ok: false, error: "forbidden" }));
@@ -13785,22 +14080,22 @@ var init_effort_mode_config = __esm({
13785
14080
  import { randomUUID as randomUUID7 } from "node:crypto";
13786
14081
  import fs11 from "node:fs";
13787
14082
  import os4 from "node:os";
13788
- import path19 from "node:path";
14083
+ import path20 from "node:path";
13789
14084
  import { fileURLToPath as fileURLToPath6 } from "node:url";
13790
14085
  function userCacheRoot() {
13791
14086
  try {
13792
14087
  const home = os4.homedir();
13793
- if (home) return path19.join(home, ".claude");
14088
+ if (home) return path20.join(home, ".claude");
13794
14089
  } catch {
13795
14090
  }
13796
- return path19.join(os4.tmpdir(), `vo-model-registry-${randomUUID7()}`);
14091
+ return path20.join(os4.tmpdir(), `vo-model-registry-${randomUUID7()}`);
13797
14092
  }
13798
14093
  function resolveCacheBaseDir(env2 = process.env, moduleDir = __dirname) {
13799
14094
  if (env2.VO_MODEL_REGISTRY_CACHE_DIR) return env2.VO_MODEL_REGISTRY_CACHE_DIR;
13800
14095
  if (env2.VO_RUNNER_RUNTIME_ROOT) return env2.VO_RUNNER_RUNTIME_ROOT;
13801
- const segments = moduleDir.split(path19.sep);
14096
+ const segments = moduleDir.split(path20.sep);
13802
14097
  const isRepoCheckout2 = segments.at(-1) === "virtual-office" && segments.at(-2) === "scripts";
13803
- return isRepoCheckout2 ? path19.resolve(moduleDir, "..", "..") : userCacheRoot();
14098
+ return isRepoCheckout2 ? path20.resolve(moduleDir, "..", "..") : userCacheRoot();
13804
14099
  }
13805
14100
  function uniqueModels(models = []) {
13806
14101
  return [...new Set(models.map((model) => String(model || "").trim()).filter(Boolean))];
@@ -13923,7 +14218,7 @@ function readCache(cacheFile = DEFAULT_CACHE_FILE, nowMs = Date.now(), ttlMs = D
13923
14218
  }
13924
14219
  }
13925
14220
  function writeCache(cacheFile = DEFAULT_CACHE_FILE, payload) {
13926
- fs11.mkdirSync(path19.dirname(cacheFile), { recursive: true });
14221
+ fs11.mkdirSync(path20.dirname(cacheFile), { recursive: true });
13927
14222
  fs11.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
13928
14223
  }
13929
14224
  async function fetchRegistryCatalog({
@@ -13981,13 +14276,13 @@ var __dirname, DEFAULT_CACHE_DIR, DEFAULT_CACHE_FILE, DEFAULT_TTL_MS3, ANTHROPIC
13981
14276
  var init_model_registry = __esm({
13982
14277
  "../../scripts/virtual-office/model-registry.mjs"() {
13983
14278
  "use strict";
13984
- __dirname = path19.dirname(fileURLToPath6(import.meta.url));
13985
- DEFAULT_CACHE_DIR = path19.join(
14279
+ __dirname = path20.dirname(fileURLToPath6(import.meta.url));
14280
+ DEFAULT_CACHE_DIR = path20.join(
13986
14281
  resolveCacheBaseDir(),
13987
14282
  ".virtual-office-cache",
13988
14283
  "model-registry"
13989
14284
  );
13990
- DEFAULT_CACHE_FILE = path19.join(DEFAULT_CACHE_DIR, "catalog.json");
14285
+ DEFAULT_CACHE_FILE = path20.join(DEFAULT_CACHE_DIR, "catalog.json");
13991
14286
  DEFAULT_TTL_MS3 = 60 * 60 * 1e3;
13992
14287
  ANTHROPIC_API_VERSION = "2023-06-01";
13993
14288
  FAMILY_DEFINITIONS = {
@@ -14626,18 +14921,18 @@ function operatorTierToRung(tier, difficulty, thresholds) {
14626
14921
  if (tier === "best" && difficulty >= thresholds.rungBounds.R5) return "R5";
14627
14922
  return base;
14628
14923
  }
14629
- function readCodexModelsCache({ path: path23 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync9 } = {}) {
14924
+ function readCodexModelsCache({ path: path24 = DEFAULT_CODEX_MODELS_CACHE, read = readFileSync9 } = {}) {
14630
14925
  try {
14631
- const parsed = JSON.parse(read(path23, "utf8"));
14926
+ const parsed = JSON.parse(read(path24, "utf8"));
14632
14927
  return Array.isArray(parsed?.models) ? parsed : null;
14633
14928
  } catch {
14634
14929
  return null;
14635
14930
  }
14636
14931
  }
14637
- function clampCodexEffort(effort, cache) {
14932
+ function clampCodexEffort(effort, cache2) {
14638
14933
  if (!effort) return { effort: null, degraded: false };
14639
14934
  const supported = /* @__PURE__ */ new Set();
14640
- for (const model of cache?.models || []) {
14935
+ for (const model of cache2?.models || []) {
14641
14936
  if (model?.visibility === "hide") continue;
14642
14937
  for (const lvl of model?.supported_reasoning_levels || []) {
14643
14938
  if (lvl?.effort) supported.add(lvl.effort);
@@ -14885,15 +15180,15 @@ function formatDecisionReason(decision, maxLen = 480) {
14885
15180
  const s = `[${decision.routerVersion}] ${decision.taskClass} d=${decision.difficulty} c=${decision.confidence} \u2192 ${decision.rung}/${decision.tier}${decision.effort ? ` effort=${decision.effort}` : ""} turns=${decision.maxTurns} $${decision.maxBudgetUsd}${decision.flags.length ? ` [${decision.flags.join(",")}]` : ""} :: ${decision.reasons.join("; ")}`;
14886
15181
  return s.length > maxLen ? `${s.slice(0, maxLen - 1)}\u2026` : s;
14887
15182
  }
14888
- function appendDecisionFallback(decision, { path: path23 = DECISION_FALLBACK_PATH, append = appendFileSync2, mkdir: mkdir5 = mkdirSync9, task, thresholds, roleCostInputs } = {}) {
15183
+ function appendDecisionFallback(decision, { path: path24 = DECISION_FALLBACK_PATH, append = appendFileSync2, mkdir: mkdir5 = mkdirSync9, task, thresholds, roleCostInputs } = {}) {
14889
15184
  try {
14890
- mkdir5(dirname12(path23), { recursive: true });
14891
- append(path23, `${JSON.stringify(decision)}
15185
+ mkdir5(dirname12(path24), { recursive: true });
15186
+ append(path24, `${JSON.stringify(decision)}
14892
15187
  `, "utf8");
14893
15188
  if (isRouterDecision(decision)) {
14894
15189
  try {
14895
15190
  const records = buildShadowRecords({ decision, task, thresholds: thresholds || loadThresholds(), roleCostInputs });
14896
- for (const record of records) append(path23, `${JSON.stringify(record)}
15191
+ for (const record of records) append(path24, `${JSON.stringify(record)}
14897
15192
  `, "utf8");
14898
15193
  } catch {
14899
15194
  }
@@ -15425,6 +15720,17 @@ var init_agent_process_env = __esm({
15425
15720
  // escape hatch inert — an operator who set it still got the subscription.
15426
15721
  "VO_RUNNER_PREFER_KEY",
15427
15722
  "VO_RUNNER_CLAUDE_PREFER_KEY",
15723
+ // The per-MACHINE metered-billing opt-in (RUNNER_ALLOW_API_BILLING_ENV in
15724
+ // agent-auth-attestation.mjs). Listed for the SAME reason as the line above:
15725
+ // applyApiBillingPolicy reads it off the env handed to runAgentTask, which is
15726
+ // this function's output — omit it and the operator's opt-in is stripped before
15727
+ // the policy ever sees it, so `permitted` is permanently false. That fails in
15728
+ // the SAFE direction (login), unlike #9242, but it is still an inert flag.
15729
+ //
15730
+ // A boolean flag, not a credential: it grants nothing on its own. The keys it
15731
+ // gates (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / CLAUDE_API_KEY) stay absent
15732
+ // from this allow-list and are re-injected, if at all, only by applyAuthEnv.
15733
+ "VO_RUNNER_ALLOW_API_BILLING",
15428
15734
  // The swarm tier binding (SWARM_TIER_BINDING_ENV in
15429
15735
  // packages/vo-mcp/src/swarm/tier-binding.ts). A fan-out resolves its billing
15430
15736
  // tier ONCE at admission and exports the binding so every subagent inherits
@@ -16215,6 +16521,53 @@ var init_terminal_delivery = __esm({
16215
16521
  }
16216
16522
  });
16217
16523
 
16524
+ // ../../scripts/virtual-office/code-runner/skill-result-json-schema.mjs
16525
+ function buildSkillResultJsonSchema({ expectedSkill, producedByAgent }) {
16526
+ return {
16527
+ type: "object",
16528
+ additionalProperties: false,
16529
+ properties: {
16530
+ schema_version: { const: 1 },
16531
+ skill: { const: expectedSkill },
16532
+ outcome: { enum: ["findings", "no_findings", "refused"] },
16533
+ findings: {
16534
+ type: "array",
16535
+ maxItems: MAX_FINDINGS,
16536
+ items: {
16537
+ type: "object",
16538
+ additionalProperties: false,
16539
+ properties: {
16540
+ claim: { type: "string", minLength: 1, maxLength: 500 },
16541
+ evidence: { type: "string", minLength: 1, maxLength: 2e3 },
16542
+ source: { type: "string", maxLength: 500 },
16543
+ confidence: { enum: ["high", "medium", "low"] }
16544
+ },
16545
+ required: ["claim", "evidence", "confidence"]
16546
+ }
16547
+ },
16548
+ findings_truncated: { type: "boolean" },
16549
+ summary: { type: "string", minLength: 1, maxLength: 2e3 },
16550
+ produced_by_agent: { const: producedByAgent }
16551
+ },
16552
+ required: [
16553
+ "schema_version",
16554
+ "skill",
16555
+ "outcome",
16556
+ "findings",
16557
+ "findings_truncated",
16558
+ "summary",
16559
+ "produced_by_agent"
16560
+ ]
16561
+ };
16562
+ }
16563
+ var MAX_FINDINGS;
16564
+ var init_skill_result_json_schema = __esm({
16565
+ "../../scripts/virtual-office/code-runner/skill-result-json-schema.mjs"() {
16566
+ "use strict";
16567
+ MAX_FINDINGS = 50;
16568
+ }
16569
+ });
16570
+
16218
16571
  // ../../scripts/virtual-office/code-runner/skill-task-runner.mjs
16219
16572
  import { createHash as createHash9 } from "node:crypto";
16220
16573
  import { mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
@@ -16275,13 +16628,7 @@ function skillResultPayloadSha256(result) {
16275
16628
  };
16276
16629
  return createHash9("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
16277
16630
  }
16278
- function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16279
- let value;
16280
- try {
16281
- value = JSON.parse(String(text ?? "").trim());
16282
- } catch {
16283
- throw new Error("skill agent did not return one strict JSON result");
16284
- }
16631
+ function parseSkillResultValue(value, { expectedSkill, producedByAgent }) {
16285
16632
  if (!exactKeys(value, ["schema_version", "skill", "outcome", "findings", "findings_truncated", "summary", "produced_by_agent"])) {
16286
16633
  throw new Error("skill result shape is invalid");
16287
16634
  }
@@ -16290,7 +16637,7 @@ function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16290
16637
  if (value.produced_by_agent !== producedByAgent) throw new Error("skill result agent attribution is invalid");
16291
16638
  if (typeof value.findings_truncated !== "boolean") throw new Error("skill result truncation flag is invalid");
16292
16639
  boundedString(value.summary, 1, 2e3, "skill result summary");
16293
- if (!Array.isArray(value.findings) || value.findings.length > MAX_FINDINGS) throw new Error("skill result findings are invalid");
16640
+ if (!Array.isArray(value.findings) || value.findings.length > MAX_FINDINGS2) throw new Error("skill result findings are invalid");
16294
16641
  if (value.outcome === "findings" ? value.findings.length === 0 : value.findings.length > 0) {
16295
16642
  throw new Error("skill result findings do not match its outcome");
16296
16643
  }
@@ -16307,6 +16654,27 @@ function parseSkillResult(text, { expectedSkill, producedByAgent }) {
16307
16654
  }
16308
16655
  return value;
16309
16656
  }
16657
+ function assertSkillRunWithinDispatch(run, { maxTurns, maxBudgetUsd } = {}) {
16658
+ if (run?.terminalSubtype !== "success") {
16659
+ throw new Error(`skill agent terminal subtype was not success (${run?.terminalSubtype || "unknown"})`);
16660
+ }
16661
+ if (Number.isInteger(maxTurns) && maxTurns > 0) {
16662
+ if (!Number.isInteger(run?.numTurns) || run.numTurns < 0) {
16663
+ throw new Error("skill agent did not report valid turn usage for the active ceiling");
16664
+ }
16665
+ if (run.numTurns > maxTurns) {
16666
+ throw new Error(`skill agent exceeded the turn ceiling (${run.numTurns} > ${maxTurns})`);
16667
+ }
16668
+ }
16669
+ if (typeof maxBudgetUsd === "number" && Number.isFinite(maxBudgetUsd) && maxBudgetUsd > 0) {
16670
+ if (typeof run?.costUsd !== "number" || !Number.isFinite(run.costUsd) || run.costUsd < 0) {
16671
+ throw new Error("skill agent did not report valid cost usage for the active ceiling");
16672
+ }
16673
+ if (run.costUsd > maxBudgetUsd) {
16674
+ throw new Error(`skill agent exceeded the cost ceiling (${run.costUsd} > ${maxBudgetUsd})`);
16675
+ }
16676
+ }
16677
+ }
16310
16678
  function composeSkillTaskPrompt({ skillBody, invocation, taskPrompt, producedByAgent }) {
16311
16679
  const body = boundedString(skillBody, 1, MAX_SKILL_BODY_CHARS, "skill body");
16312
16680
  const request = boundedString(taskPrompt, 1, 810201, "skill task request");
@@ -16341,8 +16709,9 @@ async function processSkillTask(client, task, cfg, {
16341
16709
  swarmAdmission = null,
16342
16710
  runTask = runAgentTask,
16343
16711
  resolveDispatch = resolveEffortDispatch,
16712
+ checkSkillCapability = ({ runner, bin, env: capabilityEnv }) => typeof runner.checkSkillCapability === "function" ? runner.checkSkillCapability({ bin, env: capabilityEnv }) : { compatible: false, reason: "resolved runner has no restricted-skill capability probe" },
16344
16713
  createScratch = () => mkdtemp2(join19(tmpdir(), "algohq-skill-task-")),
16345
- removeScratch = (path23) => rm2(path23, { recursive: true, force: true })
16714
+ removeScratch = (path24) => rm2(path24, { recursive: true, force: true })
16346
16715
  } = {}) {
16347
16716
  const id = task.code_task_id;
16348
16717
  let run = null;
@@ -16389,12 +16758,25 @@ async function processSkillTask(client, task, cfg, {
16389
16758
  if (selected.agent !== "claude") {
16390
16759
  throw new Error("skill execution requires the policy-restricted Claude runner");
16391
16760
  }
16761
+ const capability = await checkSkillCapability({
16762
+ runner: selected.runner,
16763
+ bin: selected.runnerBin,
16764
+ env: env2
16765
+ });
16766
+ if (!capability?.compatible) {
16767
+ throw new Error(`resolved Claude CLI is incompatible with restricted skill execution: ${capability?.reason || "unknown capability"}`);
16768
+ }
16769
+ const executionBin = capability.resolvedBin || selected.runnerBin;
16392
16770
  const basePrompt = composeSkillTaskPrompt({
16393
16771
  skillBody: skill.body,
16394
16772
  invocation,
16395
16773
  taskPrompt: task.prompt,
16396
16774
  producedByAgent: selected.agent
16397
16775
  });
16776
+ const structuredOutputSchema = buildSkillResultJsonSchema({
16777
+ expectedSkill: invocation.skill,
16778
+ producedByAgent: selected.agent
16779
+ });
16398
16780
  const dispatch = await resolveDispatch({ client, task, agent: selected.agent, env: env2, basePrompt });
16399
16781
  assertRunnerGovernors({ agent: selected.agent, task });
16400
16782
  scratch = await createScratch();
@@ -16412,10 +16794,11 @@ async function processSkillTask(client, task, cfg, {
16412
16794
  });
16413
16795
  run = await runTask({
16414
16796
  runner: selected.runner,
16415
- bin: selected.runnerBin,
16797
+ bin: executionBin,
16416
16798
  prompt: dispatch.prompt,
16417
16799
  cwd: scratch,
16418
16800
  toolPolicy: frozenInputsOnly ? "frozen_inputs_only" : "skill_readonly",
16801
+ structuredOutputSchema,
16419
16802
  permissionMode: dispatch.permissionMode,
16420
16803
  maxTurns: selected.agent === "claude" ? dispatch.maxTurns : void 0,
16421
16804
  model: dispatch.model,
@@ -16466,7 +16849,11 @@ async function processSkillTask(client, task, cfg, {
16466
16849
  } });
16467
16850
  return;
16468
16851
  }
16469
- const parsedResult = parseSkillResult(resultText(run), {
16852
+ assertSkillRunWithinDispatch(run, dispatch);
16853
+ if (!run.structuredOutput || typeof run.structuredOutput !== "object" || Array.isArray(run.structuredOutput)) {
16854
+ throw new Error("skill agent did not return provider-validated structured output");
16855
+ }
16856
+ const parsedResult = parseSkillResultValue(run.structuredOutput, {
16470
16857
  expectedSkill: invocation.skill,
16471
16858
  producedByAgent: selected.agent
16472
16859
  });
@@ -16509,7 +16896,7 @@ async function processSkillTask(client, task, cfg, {
16509
16896
  } });
16510
16897
  }
16511
16898
  }
16512
- var SKILL_SUCCESS_STATUS, SKILL_NAME_RE, INPUT_KEY_RE, MAX_SKILL_BODY_CHARS, MAX_INPUTS, MAX_FINDINGS, SHA256_RE;
16899
+ var SKILL_SUCCESS_STATUS, SKILL_NAME_RE, INPUT_KEY_RE, MAX_SKILL_BODY_CHARS, MAX_INPUTS, MAX_FINDINGS2, SHA256_RE;
16513
16900
  var init_skill_task_runner = __esm({
16514
16901
  "../../scripts/virtual-office/code-runner/skill-task-runner.mjs"() {
16515
16902
  "use strict";
@@ -16523,12 +16910,13 @@ var init_skill_task_runner = __esm({
16523
16910
  init_killed_run_outcome();
16524
16911
  init_terminal_delivery();
16525
16912
  init_cancelled_run_report();
16913
+ init_skill_result_json_schema();
16526
16914
  SKILL_SUCCESS_STATUS = "no_changes_needed";
16527
16915
  SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}(?::[a-z0-9][a-z0-9-]{0,62})?$/u;
16528
16916
  INPUT_KEY_RE = /^[a-z][a-z0-9_]{0,39}$/u;
16529
16917
  MAX_SKILL_BODY_CHARS = 256e3;
16530
16918
  MAX_INPUTS = 12;
16531
- MAX_FINDINGS = 50;
16919
+ MAX_FINDINGS2 = 50;
16532
16920
  SHA256_RE = /^[a-f0-9]{64}$/u;
16533
16921
  }
16534
16922
  });
@@ -16536,7 +16924,7 @@ var init_skill_task_runner = __esm({
16536
16924
  // ../../scripts/virtual-office/code-runner/isolation-audit.mjs
16537
16925
  import fs12 from "node:fs";
16538
16926
  import fsp11 from "node:fs/promises";
16539
- import path20 from "node:path";
16927
+ import path21 from "node:path";
16540
16928
  async function defaultRun3(command, args, cwd, options = {}) {
16541
16929
  return runProcess2(command, args, { cwd, timeout: 6e4, ...options });
16542
16930
  }
@@ -16549,7 +16937,7 @@ async function canonicalRootForWorktree(worktreeDir, run) {
16549
16937
  "--path-format=absolute",
16550
16938
  "--git-common-dir"
16551
16939
  ])).trim();
16552
- const root = path20.dirname(commonDir);
16940
+ const root = path21.dirname(commonDir);
16553
16941
  return samePath3(root, worktreeDir) ? null : root;
16554
16942
  }
16555
16943
  async function snapshot(root, run) {
@@ -16591,21 +16979,21 @@ async function changedPaths(root, run) {
16591
16979
  }
16592
16980
  async function quarantineCanonicalWrites({ baseline, worktreeDir, taskId, run, now }) {
16593
16981
  const paths = await changedPaths(baseline.root, run);
16594
- const quarantineDir = path20.join(
16595
- path20.dirname(worktreeDir),
16982
+ const quarantineDir = path21.join(
16983
+ path21.dirname(worktreeDir),
16596
16984
  ".canonical-recovery",
16597
16985
  `${String(taskId || "unknown").replace(/[^a-z0-9-]/gi, "-")}-${now().toISOString().replace(/[:.]/g, "-")}`
16598
16986
  );
16599
16987
  await fsp11.mkdir(quarantineDir, { recursive: true });
16600
16988
  const patch = await git2(run, baseline.root, ["diff", "--binary", "HEAD"], { raw: true });
16601
- await fsp11.writeFile(path20.join(quarantineDir, "tracked.patch"), patch, "utf8");
16989
+ await fsp11.writeFile(path21.join(quarantineDir, "tracked.patch"), patch, "utf8");
16602
16990
  for (const relative of paths.untracked) {
16603
- const source = path20.join(baseline.root, relative);
16604
- const target = path20.join(quarantineDir, "untracked", relative);
16605
- await fsp11.mkdir(path20.dirname(target), { recursive: true });
16991
+ const source = path21.join(baseline.root, relative);
16992
+ const target = path21.join(quarantineDir, "untracked", relative);
16993
+ await fsp11.mkdir(path21.dirname(target), { recursive: true });
16606
16994
  await fsp11.copyFile(source, target);
16607
16995
  }
16608
- await fsp11.writeFile(path20.join(quarantineDir, "manifest.json"), `${JSON.stringify({
16996
+ await fsp11.writeFile(path21.join(quarantineDir, "manifest.json"), `${JSON.stringify({
16609
16997
  taskId,
16610
16998
  canonicalRoot: baseline.root,
16611
16999
  canonicalHead: baseline.head,
@@ -16627,8 +17015,8 @@ async function restoreExactCanonicalPaths(baseline, evidence, run) {
16627
17015
  ]);
16628
17016
  }
16629
17017
  for (const relative of evidence.untracked) {
16630
- const target = path20.resolve(baseline.root, relative);
16631
- const prefix = `${path20.resolve(baseline.root)}${path20.sep}`;
17018
+ const target = path21.resolve(baseline.root, relative);
17019
+ const prefix = `${path21.resolve(baseline.root)}${path21.sep}`;
16632
17020
  if (!target.startsWith(prefix) || !fs12.existsSync(target)) continue;
16633
17021
  await fsp11.rm(target, { force: true });
16634
17022
  }
@@ -16665,7 +17053,7 @@ var init_isolation_audit = __esm({
16665
17053
  init_process_runner2();
16666
17054
  splitZ2 = (value) => String(value || "").split("\0").map((item) => item.trim()).filter(Boolean);
16667
17055
  samePath3 = (left, right) => {
16668
- const [a, b] = [left, right].map((value) => path20.resolve(value));
17056
+ const [a, b] = [left, right].map((value) => path21.resolve(value));
16669
17057
  return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
16670
17058
  };
16671
17059
  }
@@ -16830,6 +17218,174 @@ var init_outcome_commit = __esm({
16830
17218
  }
16831
17219
  });
16832
17220
 
17221
+ // ../../scripts/virtual-office/code-runner/capped-run-classification.mjs
17222
+ function isCappedRunSummary(summary) {
17223
+ return CAPPED_RESULT_SUBTYPES.includes(String(summary ?? "").trim());
17224
+ }
17225
+ function agentRequestedContinuation(run = {}) {
17226
+ return `${run?.summary ?? ""}
17227
+ ${run?.lastAgentMessage ?? ""}`.includes(PARTIAL_PR_CONTINUATION_MARKER);
17228
+ }
17229
+ function leftoverWorkAfterPublish(changedFiles) {
17230
+ if (!Array.isArray(changedFiles)) return null;
17231
+ return changedFiles.filter((file) => file && !isAgentScratch(String(file)));
17232
+ }
17233
+ function classifyCappedRunOutcome({
17234
+ partial = false,
17235
+ run = {},
17236
+ prPublished = false,
17237
+ prCarriesCommits = false,
17238
+ changedFilesAfterPublish = null,
17239
+ rateLimited = false
17240
+ } = {}) {
17241
+ const verdict = (reason, extra = {}) => ({
17242
+ needsContinuation: true,
17243
+ complete: false,
17244
+ reason,
17245
+ note: null,
17246
+ leftover: [],
17247
+ ...extra
17248
+ });
17249
+ if (!partial) {
17250
+ return { needsContinuation: false, complete: true, reason: "clean_finish", note: null, leftover: [] };
17251
+ }
17252
+ if (agentRequestedContinuation(run)) return verdict("agent_requested_continuation");
17253
+ if (rateLimited) return verdict("rate_limited_resume_pending");
17254
+ if (run?.failure) return verdict("runner_failure");
17255
+ if (run?.gateFailureNote) return verdict("completion_gate_not_passing");
17256
+ if (!isCappedRunSummary(run?.summary)) return verdict("not_a_cap_stop");
17257
+ if (!prPublished) return verdict("no_published_pr");
17258
+ if (!prCarriesCommits) return verdict("published_pr_carries_no_commits");
17259
+ const leftover = leftoverWorkAfterPublish(changedFilesAfterPublish);
17260
+ if (leftover === null) return verdict("worktree_state_unknown");
17261
+ if (leftover.length > 0) return verdict("uncommitted_work_remains", { leftover });
17262
+ return {
17263
+ needsContinuation: false,
17264
+ complete: true,
17265
+ reason: "cap_after_publish",
17266
+ note: CAP_AFTER_PUBLISH_NOTE,
17267
+ leftover: []
17268
+ };
17269
+ }
17270
+ var CAP_AFTER_PUBLISH_NOTE;
17271
+ var init_capped_run_classification = __esm({
17272
+ "../../scripts/virtual-office/code-runner/capped-run-classification.mjs"() {
17273
+ "use strict";
17274
+ init_claude_result_event();
17275
+ init_partial_pr_continuation();
17276
+ init_publish_file_state();
17277
+ CAP_AFTER_PUBLISH_NOTE = "cap reached after publish";
17278
+ }
17279
+ });
17280
+
17281
+ // ../../scripts/virtual-office/code-runner/capped-run-publication-repair.mjs
17282
+ function stripPartialTitlePrefix(title) {
17283
+ const text = String(title ?? "").trim();
17284
+ if (!PARTIAL_TITLE_PREFIX_RE.test(text)) return null;
17285
+ const stripped = text.replace(PARTIAL_TITLE_PREFIX_RE, "").trim();
17286
+ return stripped || null;
17287
+ }
17288
+ async function measureWorktreeChanges(worktreeDir, runCommand) {
17289
+ try {
17290
+ const out = await runCommand(
17291
+ "git",
17292
+ ["-c", "core.quotepath=false", "status", "--porcelain", "-z"],
17293
+ worktreeDir,
17294
+ { timeout: 6e4, raw: true }
17295
+ );
17296
+ return parsePorcelainZ(out);
17297
+ } catch {
17298
+ return null;
17299
+ }
17300
+ }
17301
+ async function branchCarriesCommits(worktreeDir, branch, runCommand, base = "origin/main") {
17302
+ if (!branch) return false;
17303
+ try {
17304
+ const out = await runCommand(
17305
+ "git",
17306
+ ["rev-list", "--count", `${base}..${branch}`],
17307
+ worktreeDir,
17308
+ { timeout: 6e4 }
17309
+ );
17310
+ return Number.parseInt(String(out).trim(), 10) > 0;
17311
+ } catch {
17312
+ return false;
17313
+ }
17314
+ }
17315
+ async function resolveCappedRunVerdict({
17316
+ partial,
17317
+ run,
17318
+ pr,
17319
+ worktreeDir,
17320
+ runCommand,
17321
+ rateLimited = false
17322
+ }) {
17323
+ const optimistic = classifyCappedRunOutcome({
17324
+ partial,
17325
+ run,
17326
+ rateLimited,
17327
+ prPublished: true,
17328
+ prCarriesCommits: true,
17329
+ changedFilesAfterPublish: []
17330
+ });
17331
+ if (optimistic.reason !== "cap_after_publish") return optimistic;
17332
+ const prPublished = Number.isInteger(pr?.prNumber) && pr.prNumber > 0;
17333
+ const [changedFilesAfterPublish, prCarriesCommits] = await Promise.all([
17334
+ measureWorktreeChanges(worktreeDir, runCommand),
17335
+ prPublished ? branchCarriesCommits(worktreeDir, pr.branch, runCommand) : Promise.resolve(false)
17336
+ ]);
17337
+ return classifyCappedRunOutcome({
17338
+ partial: true,
17339
+ run,
17340
+ prPublished,
17341
+ prCarriesCommits,
17342
+ changedFilesAfterPublish,
17343
+ rateLimited
17344
+ });
17345
+ }
17346
+ async function repairCompletePrPresentation({
17347
+ pr,
17348
+ worktreeDir,
17349
+ githubToken = null,
17350
+ runCommand,
17351
+ log: log2 = () => {
17352
+ },
17353
+ promoteReady = true
17354
+ }) {
17355
+ const env2 = githubToken ? installationTokenEnv(githubToken) : void 0;
17356
+ const result = { retitled: null, markedReady: false };
17357
+ try {
17358
+ const raw = await runCommand("gh", ["pr", "view", String(pr.prNumber), "--json", "title"], worktreeDir, { env: env2, timeout: 6e4 });
17359
+ const stripped = stripPartialTitlePrefix(JSON.parse(String(raw || "{}"))?.title);
17360
+ if (stripped) {
17361
+ await runCommand("gh", ["pr", "edit", String(pr.prNumber), "--title", stripped], worktreeDir, { env: env2, timeout: 6e4 });
17362
+ result.retitled = stripped;
17363
+ log2(`task PR #${pr.prNumber}: dropped the PARTIAL title prefix \u2014 the cap landed after publication`);
17364
+ }
17365
+ } catch (error) {
17366
+ log2(`PR #${pr.prNumber}: could not drop the PARTIAL title prefix (${String(error?.message || error).slice(0, 160)}); outcome is still recorded complete`);
17367
+ }
17368
+ if (!promoteReady) return result;
17369
+ try {
17370
+ await markExistingPrReady(worktreeDir, pr.prNumber, { env: env2, runFn: runCommand });
17371
+ result.markedReady = true;
17372
+ } catch (error) {
17373
+ log2(`PR #${pr.prNumber}: could not promote the salvage draft to ready (${String(error?.message || error).slice(0, 160)})`);
17374
+ }
17375
+ return result;
17376
+ }
17377
+ var PARTIAL_TITLE_PREFIX_RE;
17378
+ var init_capped_run_publication_repair = __esm({
17379
+ "../../scripts/virtual-office/code-runner/capped-run-publication-repair.mjs"() {
17380
+ "use strict";
17381
+ init_publish();
17382
+ init_publish_file_state();
17383
+ init_existing_pr_publication();
17384
+ init_capped_run_classification();
17385
+ PARTIAL_TITLE_PREFIX_RE = /^⚠ PARTIAL \([^)]*\)\s*—\s*/u;
17386
+ }
17387
+ });
17388
+
16833
17389
  // ../../scripts/virtual-office/code-runner/publication-outcome.mjs
16834
17390
  async function closeCancelledReplacementPr({
16835
17391
  pr,
@@ -16956,6 +17512,20 @@ async function finalizePublishedPr({
16956
17512
  const runnerFailureRecord = serializedRunnerFailure ? JSON.parse(serializedRunnerFailure) : null;
16957
17513
  const runnerFailureMessage = runnerFailureRecord ? `blocked by ${runnerFailureRecord.code}; operator review required: ${runnerFailureRecord.operator_next_action}` : "";
16958
17514
  const fixDispatchGuard = overlapBlocked || runnerFailure ? { allowFixDispatch: false } : {};
17515
+ const exec = runCommand ?? defaultRunCommand4;
17516
+ const capVerdict = await resolveCappedRunVerdict({
17517
+ partial,
17518
+ run,
17519
+ pr,
17520
+ worktreeDir,
17521
+ runCommand: exec,
17522
+ rateLimited: Boolean(rateLimitResume)
17523
+ });
17524
+ const reclassified = partial && capVerdict.reason === "cap_after_publish";
17525
+ const effectivePartial = partial && !reclassified;
17526
+ if (partial) {
17527
+ log2(reclassified ? `task ${id}: ${CAP_AFTER_PUBLISH_NOTE} with a clean worktree \u2014 recording pr_opened, not needs_continuation` : `task ${id}: partial draft stands (${capVerdict.reason})`);
17528
+ }
16959
17529
  let resumeQueued = false;
16960
17530
  if (cfg.watchEnabled) {
16961
17531
  try {
@@ -16966,8 +17536,8 @@ async function finalizePublishedPr({
16966
17536
  taskId: id,
16967
17537
  operatorId: task.operator_id,
16968
17538
  tenantId: task.tenant_id,
16969
- needsContinuation: partial && !rateLimitResume && !runnerFailure && (task.continuation_attempt ?? 0) < (task.continuation_max_attempts ?? 3),
16970
- continuationExhausted: partial && (task.continuation_attempt ?? 0) >= (task.continuation_max_attempts ?? 3),
17539
+ needsContinuation: effectivePartial && !rateLimitResume && !runnerFailure && (task.continuation_attempt ?? 0) < (task.continuation_max_attempts ?? 3),
17540
+ continuationExhausted: effectivePartial && (task.continuation_attempt ?? 0) >= (task.continuation_max_attempts ?? 3),
16971
17541
  repairChain: task.repair_chain ?? {
16972
17542
  root_pr_number: pr.prNumber,
16973
17543
  attempt: 0,
@@ -17024,14 +17594,19 @@ async function finalizePublishedPr({
17024
17594
  log: log2,
17025
17595
  patch: {
17026
17596
  status: runnerFailure ? "failed" : "pr_opened",
17027
- message: runnerFailure ? `${runnerFailureMessage}; preserved PR ${pr.prUrl} for owner review` : `opened ${pr.prUrl}${overlapNote}${pr.newCommit === false ? " (no new commits \u2014 the branch already held every change)" : ""}`,
17597
+ message: runnerFailure ? `${runnerFailureMessage}; preserved PR ${pr.prUrl} for owner review` : `opened ${pr.prUrl}${overlapNote}${reclassified ? ` (${CAP_AFTER_PUBLISH_NOTE} \u2014 every deliverable is committed)` : ""}${pr.newCommit === false ? " (no new commits \u2014 the branch already held every change)" : ""}`,
17028
17598
  pr_url: pr.prUrl,
17029
17599
  pr_number: pr.prNumber,
17030
17600
  pr_branch: pr.branch,
17031
17601
  result: (() => {
17032
17602
  const prefix = overlapBlocked ? `[VO-PUBLISH-OVERLAP-BLOCKED: ${blockedRefs}] ` : "";
17033
17603
  const room = 2e3 - prefix.length;
17034
- return `${prefix}${partial || runnerFailure ? partialPrContinuationResult(run, room, rateLimitResume ? "rate_limited" : null) : String(run.summary).slice(0, room)}`;
17604
+ if (reclassified) {
17605
+ const report = String(run.lastAgentMessage || run.summary || "").trim();
17606
+ return `${prefix}${`${CAP_AFTER_PUBLISH_NOTE}
17607
+ ${report}`.slice(0, room)}`;
17608
+ }
17609
+ return `${prefix}${effectivePartial || runnerFailure ? partialPrContinuationResult(run, room, rateLimitResume ? "rate_limited" : null) : String(run.summary).slice(0, room)}`;
17035
17610
  })(),
17036
17611
  ...runOutcomePatch(run),
17037
17612
  ...terminalLedgerPatch(run)
@@ -17059,6 +17634,16 @@ async function finalizePublishedPr({
17059
17634
  });
17060
17635
  return posted.cancelled;
17061
17636
  }
17637
+ if (reclassified) {
17638
+ await repairCompletePrPresentation({
17639
+ pr,
17640
+ worktreeDir,
17641
+ githubToken,
17642
+ runCommand: exec,
17643
+ log: log2,
17644
+ promoteReady: !overlapBlocked && !runnerFailure
17645
+ });
17646
+ }
17062
17647
  log2(`task ${id} \u2192 PR ${pr.prUrl}`);
17063
17648
  return false;
17064
17649
  }
@@ -17076,13 +17661,15 @@ var init_publication_outcome = __esm({
17076
17661
  init_terminal_delivery();
17077
17662
  init_rate_limit_resume();
17078
17663
  init_error_message();
17664
+ init_capped_run_classification();
17665
+ init_capped_run_publication_repair();
17079
17666
  defaultRunCommand4 = (cmd, args, cwd, opts = {}) => runProcess2(cmd, args, { cwd, ...opts });
17080
17667
  }
17081
17668
  });
17082
17669
 
17083
17670
  // ../../scripts/virtual-office/code-runner/committed-scratch-cleanup.mjs
17084
17671
  import fsp12 from "node:fs/promises";
17085
- import path21 from "node:path";
17672
+ import path22 from "node:path";
17086
17673
  function defaultRun4(command, args, cwd, options = {}) {
17087
17674
  return runProcess2(command, args, { cwd, ...options });
17088
17675
  }
@@ -17090,13 +17677,13 @@ async function resolveSafeScratchTarget(worktreeDir, file) {
17090
17677
  if (!isAgentScratch(file)) {
17091
17678
  throw new Error(`refusing to remove non-scratch publication path: ${file}`);
17092
17679
  }
17093
- const root = path21.resolve(worktreeDir);
17094
- const target = path21.resolve(root, file);
17095
- const relative = path21.relative(root, target);
17096
- if (!relative || relative.startsWith(`..${path21.sep}`) || path21.isAbsolute(relative)) {
17680
+ const root = path22.resolve(worktreeDir);
17681
+ const target = path22.resolve(root, file);
17682
+ const relative = path22.relative(root, target);
17683
+ if (!relative || relative.startsWith(`..${path22.sep}`) || path22.isAbsolute(relative)) {
17097
17684
  throw new Error(`refusing to remove publication scratch outside worktree: ${file}`);
17098
17685
  }
17099
- for (let cursor = target; cursor !== root; cursor = path21.dirname(cursor)) {
17686
+ for (let cursor = target; cursor !== root; cursor = path22.dirname(cursor)) {
17100
17687
  try {
17101
17688
  if ((await fsp12.lstat(cursor)).isSymbolicLink()) {
17102
17689
  throw new Error(`refusing to follow symlink while removing publication scratch: ${file}`);
@@ -17218,7 +17805,7 @@ var init_publication_scope = __esm({
17218
17805
  // ../../scripts/virtual-office/code-runner/recovery-ledger.mjs
17219
17806
  import fs13 from "node:fs";
17220
17807
  import fsp13 from "node:fs/promises";
17221
- import path22 from "node:path";
17808
+ import path23 from "node:path";
17222
17809
  function recoveryTaskId(prompt) {
17223
17810
  const match = String(prompt || "").match(/VO_RECOVERY_FROM_CODE_TASK:\s*([0-9a-f-]{36})/i);
17224
17811
  return match ? match[1].toLowerCase() : null;
@@ -17232,10 +17819,10 @@ function cloneLeaf(repo) {
17232
17819
  function recoveryLedgerCandidates(repo, clonesRoot2) {
17233
17820
  const leaf = cloneLeaf(repo);
17234
17821
  if (!leaf || !clonesRoot2) return [];
17235
- const canonical = path22.join(clonesRoot2, leaf);
17822
+ const canonical = path23.join(clonesRoot2, leaf);
17236
17823
  return [
17237
- path22.join(clonesRoot2, ".agent-worktrees", leaf, "recovery-ledger.jsonl"),
17238
- path22.join(canonical, ".agent-worktrees", "recovery-ledger.jsonl")
17824
+ path23.join(clonesRoot2, ".agent-worktrees", leaf, "recovery-ledger.jsonl"),
17825
+ path23.join(canonical, ".agent-worktrees", "recovery-ledger.jsonl")
17239
17826
  ];
17240
17827
  }
17241
17828
  async function readLedger(file, readFile6) {
@@ -17393,6 +17980,290 @@ var init_recovery_ledger = __esm({
17393
17980
  }
17394
17981
  });
17395
17982
 
17983
+ // ../../scripts/virtual-office/code-runner/host-preflight.mjs
17984
+ function joinOutput(observation = {}) {
17985
+ return [observation.stdout, observation.stderr, observation.message].map((part) => String(part ?? "")).join("\n");
17986
+ }
17987
+ function classifyGitFetchObservation(observation = {}) {
17988
+ const text = joinOutput(observation);
17989
+ if (GIT_AUTH_RE.test(text)) return GIT_AUTH_REQUIRED;
17990
+ if (GIT_NETWORK_RE.test(text)) return GIT_NETWORK;
17991
+ return GIT_OTHER;
17992
+ }
17993
+ function classifyLoginPing(observation = {}) {
17994
+ const text = joinOutput(observation);
17995
+ if (LOGIN_EXPIRED_RE.test(text)) return LOGIN_EXPIRED;
17996
+ if (RATE_LIMITED_RE.test(text)) return RATE_LIMITED;
17997
+ if (observation.ok === true && String(observation.stdout ?? "").trim().length > 0) return OK;
17998
+ return UNKNOWN;
17999
+ }
18000
+ function reportingLines(raw) {
18001
+ return String(raw ?? "").split(/\r?\n/u).filter((line) => line.trim() !== "" && !NON_REPORTING_LINE.test(line)).map((line) => line.trim()).filter((line) => !/[`'"]$/u.test(line));
18002
+ }
18003
+ function matchSignature(line, signatures) {
18004
+ for (const signature of signatures) {
18005
+ if (signature.pattern.test(line)) return signature;
18006
+ }
18007
+ return null;
18008
+ }
18009
+ function classifyHostFailure(text) {
18010
+ for (const line of reportingLines(text)) {
18011
+ const login = matchSignature(line, LOGIN_SIGNATURES);
18012
+ if (login) return { kind: "login_expired", signature: login.id, line };
18013
+ const git4 = matchSignature(line, GIT_SIGNATURES);
18014
+ if (git4) return { kind: git4.kind, signature: git4.id, line };
18015
+ }
18016
+ return null;
18017
+ }
18018
+ function classifyHostFailureSignature(text) {
18019
+ const value = String(text ?? "");
18020
+ if (!value.trim()) return null;
18021
+ const tokenMatch = new RegExp(`${HOST_ENV_FAILURE_TOKEN}:(${LOGIN_EXPIRED}|${GIT_AUTH_REQUIRED})`, "u").exec(value);
18022
+ if (tokenMatch) return tokenMatch[1];
18023
+ const found = classifyHostFailure(value);
18024
+ if (!found) return null;
18025
+ if (found.kind === "login_expired") return LOGIN_EXPIRED;
18026
+ if (found.kind === "git_credentials") return GIT_AUTH_REQUIRED;
18027
+ return null;
18028
+ }
18029
+ function hostFailureResultText(reason, detail = "") {
18030
+ const trimmed = String(detail ?? "").replace(/\s+/gu, " ").trim().slice(0, 400);
18031
+ return `${HOST_ENV_FAILURE_TOKEN}:${reason} \u2014 ${hostHealthRemedy(reason)}${trimmed ? ` [${trimmed}]` : ""}`;
18032
+ }
18033
+ function hostHealthRemedy(reason) {
18034
+ switch (reason) {
18035
+ case LOGIN_EXPIRED:
18036
+ return "login expired: run claude auth login on this host";
18037
+ case RATE_LIMITED:
18038
+ return "agent account is rate limited: work resumes when the window resets";
18039
+ case GIT_AUTH_REQUIRED:
18040
+ return "git auth required: refresh the git credential on this host";
18041
+ case GIT_NETWORK:
18042
+ return "git cannot reach the remote: check this host network";
18043
+ case GIT_OTHER:
18044
+ return "git fetch failed on this host: see the runner log";
18045
+ case UNKNOWN:
18046
+ return "host check could not be completed";
18047
+ default:
18048
+ return "host is ready";
18049
+ }
18050
+ }
18051
+ async function capture(run, cmd, args, options) {
18052
+ try {
18053
+ const stdout = await run(cmd, args, options);
18054
+ return { ok: true, status: 0, stdout: String(stdout ?? ""), stderr: "", timedOut: false };
18055
+ } catch (error) {
18056
+ return {
18057
+ ok: false,
18058
+ status: typeof error?.status === "number" ? error.status : null,
18059
+ stdout: String(error?.stdout ?? ""),
18060
+ stderr: String(error?.stderr ?? ""),
18061
+ message: String(error?.message ?? error ?? ""),
18062
+ timedOut: error?.code === "ETIMEDOUT"
18063
+ };
18064
+ }
18065
+ }
18066
+ function nonInteractiveGitEnv(env2 = process.env) {
18067
+ return {
18068
+ ...env2,
18069
+ GIT_TERMINAL_PROMPT: "0",
18070
+ GCM_INTERACTIVE: "never",
18071
+ GIT_OPTIONAL_LOCKS: "0"
18072
+ };
18073
+ }
18074
+ async function checkGitFetch({
18075
+ cwd,
18076
+ run,
18077
+ env: env2 = process.env,
18078
+ remote = "origin",
18079
+ branch = "main",
18080
+ timeoutMs = GIT_FETCH_TIMEOUT_MS
18081
+ } = {}) {
18082
+ if (typeof run !== "function" || !cwd) {
18083
+ return { status: UNKNOWN, detail: "no runner clone to fetch from" };
18084
+ }
18085
+ const observation = await capture(run, "git", ["fetch", remote, branch], {
18086
+ cwd,
18087
+ timeout: timeoutMs,
18088
+ env: nonInteractiveGitEnv(env2)
18089
+ });
18090
+ if (observation.ok) return { status: OK, detail: `git fetch ${remote} ${branch} succeeded` };
18091
+ const reason = classifyGitFetchObservation(observation);
18092
+ const raw = joinOutput(observation).replace(/\s+/gu, " ").trim();
18093
+ return {
18094
+ status: reason,
18095
+ detail: observation.timedOut ? `git fetch ${remote} ${branch} exceeded ${Math.round(timeoutMs / 1e3)}s${raw ? `: ${raw.slice(0, 200)}` : " with no output"}` : raw.slice(0, 300) || `git fetch ${remote} ${branch} failed`
18096
+ };
18097
+ }
18098
+ async function checkAgentLogin({
18099
+ run,
18100
+ bin = "claude",
18101
+ env: env2 = process.env,
18102
+ timeoutMs = LOGIN_PING_TIMEOUT_MS,
18103
+ prompt = "Reply with the single word: ready"
18104
+ } = {}) {
18105
+ if (typeof run !== "function") {
18106
+ return { status: UNKNOWN, detail: "no login probe available on this host" };
18107
+ }
18108
+ const observation = await capture(run, bin, ["-p", prompt, "--max-turns", "1"], {
18109
+ timeout: timeoutMs,
18110
+ env: env2,
18111
+ input: ""
18112
+ });
18113
+ const reason = classifyLoginPing(observation);
18114
+ if (reason === OK) return { status: OK, detail: "one-turn agent ping authenticated" };
18115
+ const raw = joinOutput(observation).replace(/\s+/gu, " ").trim();
18116
+ return {
18117
+ status: reason,
18118
+ detail: observation.timedOut ? `agent ping exceeded ${Math.round(timeoutMs / 1e3)}s` : raw.slice(0, 300) || "agent ping produced no output"
18119
+ };
18120
+ }
18121
+ function preflightAllowsClaim(health) {
18122
+ if (!health) return true;
18123
+ return !CLAIM_BLOCKING_GIT.has(health.git) && !CLAIM_BLOCKING_LOGIN.has(health.login);
18124
+ }
18125
+ function blockingReason(health) {
18126
+ if (!health) return null;
18127
+ if (CLAIM_BLOCKING_GIT.has(health.git)) return health.git;
18128
+ if (CLAIM_BLOCKING_LOGIN.has(health.login)) return health.login;
18129
+ return null;
18130
+ }
18131
+ async function runHostPreflight({
18132
+ cwd,
18133
+ runGit,
18134
+ runAgent,
18135
+ bin = "claude",
18136
+ agent = "claude",
18137
+ env: env2 = process.env,
18138
+ now = () => Date.now(),
18139
+ gitTimeoutMs = GIT_FETCH_TIMEOUT_MS,
18140
+ loginTimeoutMs = LOGIN_PING_TIMEOUT_MS
18141
+ } = {}) {
18142
+ const git4 = await checkGitFetch({ cwd, run: runGit, env: env2, timeoutMs: gitTimeoutMs });
18143
+ const login = CLAIM_BLOCKING_GIT.has(git4.status) ? { status: UNKNOWN, detail: "skipped: git check already blocked this host" } : agent !== "claude" ? { status: UNKNOWN, detail: `login ping not implemented for agent ${agent}` } : await checkAgentLogin({ run: runAgent, bin, env: env2, timeoutMs: loginTimeoutMs });
18144
+ const health = {
18145
+ git: git4.status,
18146
+ login: login.status,
18147
+ checked_at: new Date(now()).toISOString()
18148
+ };
18149
+ const blocking = blockingReason(health);
18150
+ const detail = blocking ? `${hostHealthRemedy(blocking)} \u2014 ${blocking === git4.status ? git4.detail : login.detail}` : login.status === RATE_LIMITED ? `${hostHealthRemedy(RATE_LIMITED)} \u2014 ${login.detail}` : `${git4.detail}; ${login.detail}`;
18151
+ return { ...health, detail: detail.slice(0, 500) };
18152
+ }
18153
+ function positiveSeconds(raw, fallbackMs) {
18154
+ const value = Number(raw);
18155
+ return Number.isFinite(value) && value > 0 ? value * 1e3 : fallbackMs;
18156
+ }
18157
+ function makeHostPreflightGate({
18158
+ preflight = runHostPreflight,
18159
+ now = () => Date.now(),
18160
+ env: env2 = process.env,
18161
+ log: log2 = () => {
18162
+ },
18163
+ ...preflightOptions
18164
+ } = {}) {
18165
+ const okIntervalMs = positiveSeconds(env2.VO_HOST_PREFLIGHT_OK_SEC, PREFLIGHT_OK_INTERVAL_MS);
18166
+ const retryIntervalMs = positiveSeconds(env2.VO_HOST_PREFLIGHT_RETRY_SEC, PREFLIGHT_RETRY_MS);
18167
+ let health = null;
18168
+ let checkedAtMs = 0;
18169
+ let running = false;
18170
+ let lastLoggedReason;
18171
+ const intervalMs = () => preflightAllowsClaim(health) ? okIntervalMs : retryIntervalMs;
18172
+ return {
18173
+ /** ms of the next scheduled check; 0 before the first one has ever run. */
18174
+ nextCheckAt: () => checkedAtMs === 0 ? 0 : checkedAtMs + intervalMs(),
18175
+ get: () => health,
18176
+ allowClaim: () => preflightAllowsClaim(health),
18177
+ blockingReason: () => blockingReason(health),
18178
+ async ensure(nowMs = now()) {
18179
+ if (running) return health;
18180
+ if (checkedAtMs !== 0 && nowMs - checkedAtMs < intervalMs()) return health;
18181
+ running = true;
18182
+ try {
18183
+ health = await preflight({ ...preflightOptions, env: env2, now });
18184
+ } catch (error) {
18185
+ health = {
18186
+ git: UNKNOWN,
18187
+ login: UNKNOWN,
18188
+ checked_at: new Date(nowMs).toISOString(),
18189
+ detail: `host preflight could not run: ${String(error?.message ?? error).slice(0, 200)}`
18190
+ };
18191
+ log2(`host preflight error: ${String(error?.message ?? error)}`);
18192
+ } finally {
18193
+ checkedAtMs = nowMs;
18194
+ running = false;
18195
+ }
18196
+ const reason = blockingReason(health);
18197
+ if (reason !== lastLoggedReason) {
18198
+ lastLoggedReason = reason;
18199
+ log2(reason ? `host preflight BLOCKED claiming \u2014 ${hostHealthRemedy(reason)} (${health.detail})` : "host preflight passed \u2014 git fetch and agent login are healthy");
18200
+ }
18201
+ return health;
18202
+ }
18203
+ };
18204
+ }
18205
+ var DEFAULT_BLOCKED_RECHECK_MS, DEFAULT_HEALTHY_RECHECK_MS, MAX_RECHECK_MS, GIT_FETCH_TIMEOUT_MS, LOGIN_PING_TIMEOUT_MS, PREFLIGHT_RETRY_MS, PREFLIGHT_OK_INTERVAL_MS, GIT_AUTH_REQUIRED, GIT_NETWORK, GIT_OTHER, LOGIN_EXPIRED, RATE_LIMITED, OK, UNKNOWN, HOST_ENV_FAILURE_TOKEN, CLAIM_BLOCKING_GIT, CLAIM_BLOCKING_LOGIN, GIT_AUTH_RE, GIT_NETWORK_RE, LOGIN_EXPIRED_RE, RATE_LIMITED_RE, SEVERITY_PREFIX, LOGIN_SIGNATURES, GIT_SIGNATURES, NON_REPORTING_LINE;
18206
+ var init_host_preflight = __esm({
18207
+ "../../scripts/virtual-office/code-runner/host-preflight.mjs"() {
18208
+ "use strict";
18209
+ DEFAULT_BLOCKED_RECHECK_MS = 5 * 6e4;
18210
+ DEFAULT_HEALTHY_RECHECK_MS = 30 * 6e4;
18211
+ MAX_RECHECK_MS = 6 * 60 * 6e4;
18212
+ GIT_FETCH_TIMEOUT_MS = 6e4;
18213
+ LOGIN_PING_TIMEOUT_MS = 9e4;
18214
+ PREFLIGHT_RETRY_MS = DEFAULT_BLOCKED_RECHECK_MS;
18215
+ PREFLIGHT_OK_INTERVAL_MS = DEFAULT_HEALTHY_RECHECK_MS;
18216
+ GIT_AUTH_REQUIRED = "git_auth_required";
18217
+ GIT_NETWORK = "git_network";
18218
+ GIT_OTHER = "git_other";
18219
+ LOGIN_EXPIRED = "login_expired";
18220
+ RATE_LIMITED = "rate_limited";
18221
+ OK = "ok";
18222
+ UNKNOWN = "unknown";
18223
+ HOST_ENV_FAILURE_TOKEN = "HOST_ENV_FAILURE";
18224
+ CLAIM_BLOCKING_GIT = /* @__PURE__ */ new Set([GIT_AUTH_REQUIRED, GIT_NETWORK, GIT_OTHER]);
18225
+ CLAIM_BLOCKING_LOGIN = /* @__PURE__ */ new Set([LOGIN_EXPIRED]);
18226
+ GIT_AUTH_RE = /could not read (?:Username|Password)|terminal prompts disabled|Authentication failed|authentication failed|Permission denied \(publickey\)|Invalid username or password|Support for password authentication was removed|HTTP Basic: Access denied|\b401\b|\b403\b|Logon failed|credential(?:s)? (?:helper|manager)|no credentials|Repository not found/i;
18227
+ GIT_NETWORK_RE = /could not resolve host|couldn'?t resolve host|failed to connect|unable to access|connection (?:reset|refused|closed|timed out)|operation timed out|remote end hung up|early eof|rpc failed|recv failure|gnutls_handshake|ssl_read|\b50[234]\b|temporary failure|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH/i;
18228
+ LOGIN_EXPIRED_RE = /OAuth (?:session|token) (?:has )?expired|could not be refreshed|Failed to authenticate|Invalid API key|invalid[^.\n]{0,24}(?:credential|authentication)|authentication_error|\bunauthorized\b|\b401\b|not (?:logged in|authenticated)|please run [`'"]?claude (?:auth )?login|run [`'"]?claude (?:auth )?login/i;
18229
+ RATE_LIMITED_RE = /rate limit|rate[_-]limited|usage limit|\b429\b|too many requests|quota (?:exceeded|exhausted)|limit reached|out of (?:credits|quota)|resource[_ ]exhausted/i;
18230
+ SEVERITY_PREFIX = String.raw`(?:\[?(?:error|Error|ERROR)\]?:?\s+)?`;
18231
+ LOGIN_SIGNATURES = [
18232
+ {
18233
+ id: "oauth_session_expired",
18234
+ pattern: new RegExp(
18235
+ `^${SEVERITY_PREFIX}Failed to authenticate: OAuth session expired and could not be refreshed\\.?$`,
18236
+ "u"
18237
+ )
18238
+ },
18239
+ {
18240
+ id: "invalid_credentials",
18241
+ pattern: new RegExp(`^${SEVERITY_PREFIX}Invalid authentication credentials\\.?$`, "u")
18242
+ }
18243
+ ];
18244
+ GIT_SIGNATURES = [
18245
+ {
18246
+ id: "git_fetch_timeout",
18247
+ kind: "git_unreachable",
18248
+ pattern: new RegExp(
18249
+ `^${SEVERITY_PREFIX}git fetch origin main failed: timed out\\.?$`,
18250
+ "u"
18251
+ )
18252
+ },
18253
+ {
18254
+ // fatal: could not read Username for 'https://github.com': terminal prompts disabled
18255
+ id: "git_credential_prompt",
18256
+ kind: "git_credentials",
18257
+ pattern: new RegExp(
18258
+ String.raw`^fatal:\s+could not read (?:Username|Password) for '[^']*':\s*` + String.raw`(?:terminal prompts disabled|No such device or address|Device not configured)\.?$`,
18259
+ "u"
18260
+ )
18261
+ }
18262
+ ];
18263
+ NON_REPORTING_LINE = /^\s*(?:[+\->*#|]|\/\/|\/\*|\*\/|\d+[.)]\s|`{1,3}|'|")/u;
18264
+ }
18265
+ });
18266
+
17396
18267
  // ../../scripts/virtual-office/code-runner/no-changes-terminal-status.mjs
17397
18268
  function defaultRunCommand5(cmd, args, cwd, opts = {}) {
17398
18269
  return runProcess2(cmd, args, { cwd, ...opts });
@@ -17414,6 +18285,14 @@ function isMaxTurnExhaustion(run = {}, maxTurns) {
17414
18285
  }
17415
18286
  function decideNoChangesTerminalStatus({ partial, run = {}, maxTurns } = {}) {
17416
18287
  const structuredFailure = serializeRunnerFailure(run.failure);
18288
+ const hostFailure = classifyHostFailureSignature(run.summary) ?? classifyHostFailureSignature(structuredFailure);
18289
+ if (hostFailure) {
18290
+ return {
18291
+ status: "failed",
18292
+ message: `host cannot run work \u2014 ${hostHealthRemedy(hostFailure)}`,
18293
+ result: hostFailureResultText(hostFailure, run.summary).slice(0, RESULT_LIMIT)
18294
+ };
18295
+ }
17417
18296
  if (!partial) {
17418
18297
  if (structuredFailure) {
17419
18298
  const failure = JSON.parse(structuredFailure);
@@ -17582,6 +18461,7 @@ var init_no_changes_terminal_status = __esm({
17582
18461
  init_terminal_delivery();
17583
18462
  init_error_message();
17584
18463
  init_partial_pr_continuation();
18464
+ init_host_preflight();
17585
18465
  RESULT_LIMIT = 2e3;
17586
18466
  }
17587
18467
  });
@@ -17873,6 +18753,8 @@ async function processOneTask(client, task, cfg, runnerInstanceId, swarmAdmissio
17873
18753
  env: buildAgentProcessEnv(process.env, { agent: sel.agent, runnerId: cfg.runnerId, taskId: id, repo: task.repo, githubReadToken: agentGithubReadToken, swarmAdmission }),
17874
18754
  // swarmAdmission mints VO_SWARM_TIER_BINDING: ONE tier decision for this task's whole agent tree
17875
18755
  sandbox,
18756
+ allowApiBilling: task.allow_api_billing === true,
18757
+ // per-TASK grant; AND-ed with the per-machine VO_RUNNER_ALLOW_API_BILLING inside applyApiBillingPolicy
17876
18758
  onProgress: (text, checkpoint) => {
17877
18759
  const usage = checkpoint?.tokenUsage ? { token_usage: checkpoint.tokenUsage } : {};
17878
18760
  const patch = text ? runnerStagePatch("agent_working", text, usage) : { stage: "agent_working", ...usage };
@@ -18103,7 +18985,8 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
18103
18985
  const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log(`agent probe failed: ${e.message}`) });
18104
18986
  await agentAvailability.ready();
18105
18987
  const accountUsage = makeAccountUsageProvider();
18106
- const loopTick = makeLoopTicks({ client, cfg, env: env2, log, getActive: () => active, runnerInstanceId, capacityController, localModelController: createLocalModelRemoteController({ env: env2, log }), preparedJobController: createPreparedJobRemoteController({ log }), getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
18988
+ const hostGate = makeHostPreflightGate({ cwd: process.cwd(), runGit: runProcess2, runAgent: runProcess2, bin: cfg.runnerBin, agent: cfg.agent, env: env2, log });
18989
+ const loopTick = makeLoopTicks({ client, cfg, env: env2, log, getActive: () => active, runnerInstanceId, capacityController, localModelController: createLocalModelRemoteController({ env: env2, log }), preparedJobController: createPreparedJobRemoteController({ log }), getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get(), getHostHealth: () => hostGate.get() });
18107
18990
  const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log });
18108
18991
  let detachedFlushRunning = false;
18109
18992
  while (!stopping) {
@@ -18113,9 +18996,10 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
18113
18996
  detachedFlushRunning = false;
18114
18997
  });
18115
18998
  }
18999
+ await hostGate.ensure();
18116
19000
  const heartbeatCompletion = loopTick();
18117
19001
  if (watchCyclesEnabled) watchCoordinator.start();
18118
- const claimAgents = resolveAgentClaimContext(agentAvailability, cfg.agent);
19002
+ const claimAgents = hostGate.allowClaim() ? resolveAgentClaimContext(agentAvailability, cfg.agent) : null;
18119
19003
  if (!claimAgents) {
18120
19004
  await heartbeatCompletion;
18121
19005
  await sleep2(cfg.pollSec * 1e3);
@@ -18132,7 +19016,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
18132
19016
  }
18133
19017
  let task;
18134
19018
  try {
18135
- task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale, ...claimAgents });
19019
+ task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale, ...claimAgents, agentAuthSource: resolveRunnerAttestedAuthSource(process.env) });
18136
19020
  reconcileStale = false;
18137
19021
  backoff.onSuccess();
18138
19022
  } catch (err) {
@@ -18152,6 +19036,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
18152
19036
  continue;
18153
19037
  }
18154
19038
  log(`claimed task ${task.code_task_id} (${task.repo})`);
19039
+ log(`task ${task.code_task_id} ${describeTaskAuthSource(process.env, task)}`);
18155
19040
  active += 1;
18156
19041
  activeTaskIds.add(task.code_task_id);
18157
19042
  const runTask = selectTaskProcessor(task, {
@@ -18224,6 +19109,7 @@ var init_code_runner_daemon = __esm({
18224
19109
  init_task_helpers();
18225
19110
  init_prepared_job_shadow();
18226
19111
  init_agent_process_env();
19112
+ init_agent_auth_attestation();
18227
19113
  init_sandbox_config();
18228
19114
  init_inference_task_runner();
18229
19115
  init_skill_task_runner();
@@ -18241,6 +19127,8 @@ var init_code_runner_daemon = __esm({
18241
19127
  init_daemon_config();
18242
19128
  init_publication_scope();
18243
19129
  init_task_worktree_preparation();
19130
+ init_process_runner2();
19131
+ init_host_preflight();
18244
19132
  init_detached_economics_spool();
18245
19133
  RATE_LIMIT_RESUME_ENABLED = process.env.VO_RATE_LIMIT_RESUME !== "0";
18246
19134
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));