@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.
- package/dist/agent-auth-probe-cli.mjs +196 -11
- package/dist/cli.js +110 -8
- package/dist/cli.js.map +2 -2
- package/dist/index.js +110 -8
- package/dist/index.js.map +2 -2
- package/dist/runner-cli.js +1386 -498
- package/dist/runner-cli.js.map +3 -3
- package/dist/runner-supervisor.js +116 -57
- package/dist/runner-supervisor.js.map +3 -3
- package/package.json +1 -1
|
@@ -300,7 +300,7 @@ function normalizeClaudePermissionMode(value) {
|
|
|
300
300
|
}
|
|
301
301
|
return normalized;
|
|
302
302
|
}
|
|
303
|
-
function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", env = process.env } = {}) {
|
|
303
|
+
function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", structuredOutputSchema, env = process.env } = {}) {
|
|
304
304
|
const effectivePermissionMode = normalizeClaudePermissionMode(permissionMode);
|
|
305
305
|
if (!["default", "skill_readonly", "frozen_inputs_only"].includes(toolPolicy)) {
|
|
306
306
|
throw new Error(`unsupported Claude tool policy "${toolPolicy}"`);
|
|
@@ -338,6 +338,12 @@ function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, m
|
|
|
338
338
|
"none"
|
|
339
339
|
);
|
|
340
340
|
}
|
|
341
|
+
if (structuredOutputSchema !== void 0) {
|
|
342
|
+
if (!restrictedSkill || !structuredOutputSchema || typeof structuredOutputSchema !== "object" || Array.isArray(structuredOutputSchema)) {
|
|
343
|
+
throw new Error("structured output schema is allowed only for a restricted skill");
|
|
344
|
+
}
|
|
345
|
+
args.push("--json-schema", JSON.stringify(structuredOutputSchema));
|
|
346
|
+
}
|
|
341
347
|
if (frozenInputsOnly) {
|
|
342
348
|
args.push("--strict-mcp-config", "--safe-mode");
|
|
343
349
|
}
|
|
@@ -425,13 +431,15 @@ function extractModelUsage(evt) {
|
|
|
425
431
|
// ../../scripts/virtual-office/code-runner/claude-result-event.mjs
|
|
426
432
|
var CAPPED_RESULT_SUBTYPES = Object.freeze(["error_max_budget_usd", "error_max_turns"]);
|
|
427
433
|
function buildResultEvent(evt) {
|
|
428
|
-
const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
|
|
434
|
+
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";
|
|
429
435
|
return {
|
|
430
436
|
kind: "result",
|
|
431
437
|
isError,
|
|
432
438
|
costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
|
|
433
439
|
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
440
|
+
terminalSubtype: typeof evt.subtype === "string" ? evt.subtype : null,
|
|
434
441
|
numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null,
|
|
442
|
+
structuredOutput: Object.hasOwn(evt, "structured_output") ? evt.structured_output : null,
|
|
435
443
|
tokenUsage: extractTokenUsage(evt),
|
|
436
444
|
modelUsage: extractModelUsage(evt)
|
|
437
445
|
};
|
|
@@ -544,9 +552,145 @@ function applyCliVersionFloor({ versionOutput, env = process.env, log = console.
|
|
|
544
552
|
return { refused: !allowUnsafe, check, message };
|
|
545
553
|
}
|
|
546
554
|
|
|
555
|
+
// ../../scripts/virtual-office/code-runner/claude-skill-capability.mjs
|
|
556
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
557
|
+
import { accessSync, constants, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
558
|
+
import path3 from "node:path";
|
|
559
|
+
var VALIDATED_CLAUDE_SKILL_VERSIONS = Object.freeze(["2.1.263"]);
|
|
560
|
+
var REQUIRED_CLAUDE_SKILL_HELP = Object.freeze([
|
|
561
|
+
"--allowedTools",
|
|
562
|
+
"--disable-slash-commands",
|
|
563
|
+
"--json-schema",
|
|
564
|
+
"--max-budget-usd",
|
|
565
|
+
"--no-chrome",
|
|
566
|
+
"--no-session-persistence",
|
|
567
|
+
"--output-format",
|
|
568
|
+
"--permission-mode",
|
|
569
|
+
"--permission-prompts",
|
|
570
|
+
"--safe-mode",
|
|
571
|
+
"--strict-mcp-config",
|
|
572
|
+
"--tools"
|
|
573
|
+
]);
|
|
574
|
+
var PROBE_TIMEOUT_MS = 2e3;
|
|
575
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
576
|
+
var cache = /* @__PURE__ */ new Map();
|
|
577
|
+
function hasOption(help, option) {
|
|
578
|
+
const literal = option.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
579
|
+
return new RegExp(`(^|\\s)${literal}(?=\\s|,|=|<|$)`, "mu").test(help);
|
|
580
|
+
}
|
|
581
|
+
function assessClaudeSkillCapability({ versionOutput, helpOutput }) {
|
|
582
|
+
const version = parseCliVersion(versionOutput);
|
|
583
|
+
if (!version || !VALIDATED_CLAUDE_SKILL_VERSIONS.includes(version)) {
|
|
584
|
+
return { compatible: false, version, reason: "claude version is not in the validated restricted-skill manifest" };
|
|
585
|
+
}
|
|
586
|
+
const help = String(helpOutput ?? "");
|
|
587
|
+
const missing = REQUIRED_CLAUDE_SKILL_HELP.filter((option) => !hasOption(help, option));
|
|
588
|
+
if (missing.length > 0) {
|
|
589
|
+
return { compatible: false, version, reason: `claude help is missing required options: ${missing.join(", ")}` };
|
|
590
|
+
}
|
|
591
|
+
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)) {
|
|
592
|
+
return { compatible: false, version, reason: "claude help does not prove required none/stream-json values" };
|
|
593
|
+
}
|
|
594
|
+
return { compatible: true, version, reason: "validated restricted-skill CLI contract" };
|
|
595
|
+
}
|
|
596
|
+
function runProbe(bin, args, env, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
597
|
+
if (process.platform === "win32") {
|
|
598
|
+
try {
|
|
599
|
+
const launch = buildWindowsClaudeLaunch({ bin, args, env });
|
|
600
|
+
return spawnSync6(launch.bin, launch.args, {
|
|
601
|
+
...launch.spawnOptions,
|
|
602
|
+
env,
|
|
603
|
+
encoding: "utf8",
|
|
604
|
+
timeout: timeoutMs
|
|
605
|
+
});
|
|
606
|
+
} catch (error) {
|
|
607
|
+
return { status: null, stdout: "", stderr: "", error };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return spawnSync6(bin, args, { env, encoding: "utf8", timeout: timeoutMs, windowsHide: true });
|
|
611
|
+
}
|
|
612
|
+
function probeText(probe) {
|
|
613
|
+
return `${String(probe?.stdout ?? "")}
|
|
614
|
+
${String(probe?.stderr ?? "")}`.trim();
|
|
615
|
+
}
|
|
616
|
+
function resolveClaudeBinaryIdentity(bin = "claude", env = process.env) {
|
|
617
|
+
let resolvedBin = String(bin);
|
|
618
|
+
try {
|
|
619
|
+
if (process.platform === "win32") {
|
|
620
|
+
resolvedBin = buildWindowsClaudeLaunch({ bin: resolvedBin, args: [], env }).bin;
|
|
621
|
+
} else if (!path3.isAbsolute(resolvedBin)) {
|
|
622
|
+
const found = String(env?.PATH ?? "").split(path3.delimiter).find((dir) => {
|
|
623
|
+
try {
|
|
624
|
+
accessSync(path3.join(dir, resolvedBin), constants.X_OK);
|
|
625
|
+
return true;
|
|
626
|
+
} catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
if (found) resolvedBin = path3.join(found, resolvedBin);
|
|
631
|
+
}
|
|
632
|
+
const canonical = realpathSync2(resolvedBin);
|
|
633
|
+
const stat = statSync(canonical);
|
|
634
|
+
return { resolvedBin: canonical, fingerprint: `${canonical}\0${stat.size}\0${stat.mtimeMs}` };
|
|
635
|
+
} catch {
|
|
636
|
+
const pathValue2 = String(env?.PATH ?? env?.Path ?? "");
|
|
637
|
+
return { resolvedBin, fingerprint: `${resolvedBin}\0${pathValue2}` };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function probeClaudeSkillCapability({
|
|
641
|
+
bin = "claude",
|
|
642
|
+
env = process.env,
|
|
643
|
+
versionOutput,
|
|
644
|
+
spawnProbe = runProbe,
|
|
645
|
+
now = () => Date.now(),
|
|
646
|
+
cacheTtlMs = CACHE_TTL_MS,
|
|
647
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
648
|
+
freshIdentity = false,
|
|
649
|
+
resolveIdentity = resolveClaudeBinaryIdentity
|
|
650
|
+
} = {}) {
|
|
651
|
+
const identity = resolveIdentity(bin, env);
|
|
652
|
+
const key = identity.fingerprint;
|
|
653
|
+
const existing = cache.get(key);
|
|
654
|
+
if (!freshIdentity && versionOutput === void 0 && existing && now() - existing.at < cacheTtlMs) {
|
|
655
|
+
return existing.value;
|
|
656
|
+
}
|
|
657
|
+
const versionProbe = freshIdentity || versionOutput === void 0 ? spawnProbe(identity.resolvedBin, ["--version"], env, timeoutMs) : null;
|
|
658
|
+
if (versionProbe?.error || versionProbe && versionProbe.status !== 0) {
|
|
659
|
+
return {
|
|
660
|
+
compatible: false,
|
|
661
|
+
version: null,
|
|
662
|
+
resolvedBin: identity.resolvedBin,
|
|
663
|
+
reason: "claude version capability probe failed"
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const effectiveVersionOutput = versionProbe ? probeText(versionProbe) : versionOutput;
|
|
667
|
+
const suppliedVersion = parseCliVersion(effectiveVersionOutput);
|
|
668
|
+
if (existing && now() - existing.at < cacheTtlMs && suppliedVersion === existing.value.version) {
|
|
669
|
+
return existing.value;
|
|
670
|
+
}
|
|
671
|
+
const helpProbe = spawnProbe(identity.resolvedBin, ["--help"], env, timeoutMs);
|
|
672
|
+
if (helpProbe?.error || helpProbe?.status !== 0) {
|
|
673
|
+
return {
|
|
674
|
+
compatible: false,
|
|
675
|
+
version: suppliedVersion,
|
|
676
|
+
resolvedBin: identity.resolvedBin,
|
|
677
|
+
reason: "claude help capability probe failed"
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const assessed = assessClaudeSkillCapability({
|
|
681
|
+
versionOutput: effectiveVersionOutput,
|
|
682
|
+
helpOutput: probeText(helpProbe)
|
|
683
|
+
});
|
|
684
|
+
const value = { ...assessed, resolvedBin: identity.resolvedBin };
|
|
685
|
+
cache.set(key, { at: now(), value });
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
|
|
547
689
|
// ../../scripts/virtual-office/code-runner/claude-auth-check.mjs
|
|
548
690
|
var FIRST_VERSION_TIMEOUT_MS = 4500;
|
|
549
691
|
var RETRY_VERSION_TIMEOUT_MS = 2e3;
|
|
692
|
+
var AUTH_PROBE_BUDGET_MS = 9500;
|
|
693
|
+
var MIN_SKILL_PROBE_MS = 250;
|
|
550
694
|
function errorCode(error) {
|
|
551
695
|
return String(error?.code || "").toUpperCase();
|
|
552
696
|
}
|
|
@@ -572,9 +716,12 @@ async function checkClaudeAuth({
|
|
|
572
716
|
spawnVersion = spawnClaudeSync,
|
|
573
717
|
probeLogin = probeClaudeLoginState,
|
|
574
718
|
getStoredKey = getAnthropicKey,
|
|
575
|
-
|
|
719
|
+
probeSkillCapability = probeClaudeSkillCapability,
|
|
720
|
+
env = process.env,
|
|
721
|
+
now = () => Date.now()
|
|
576
722
|
} = {}) {
|
|
577
723
|
try {
|
|
724
|
+
const startedAt = now();
|
|
578
725
|
let probe = spawnVersion(["--version"], {
|
|
579
726
|
timeout: FIRST_VERSION_TIMEOUT_MS,
|
|
580
727
|
encoding: "utf8",
|
|
@@ -608,23 +755,40 @@ async function checkClaudeAuth({
|
|
|
608
755
|
}
|
|
609
756
|
const floorGate = applyCliVersionFloor({ versionOutput: probe.stdout, env });
|
|
610
757
|
if (floorGate.refused) {
|
|
611
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
installed: true,
|
|
760
|
+
authenticated: false,
|
|
761
|
+
version: floorGate.check.version ?? void 0,
|
|
762
|
+
skillCapable: false,
|
|
763
|
+
message: floorGate.message
|
|
764
|
+
};
|
|
612
765
|
}
|
|
613
766
|
const loggedIn = retriedAfterTimeout ? null : probeLogin();
|
|
614
767
|
if (loggedIn === false) {
|
|
615
768
|
return {
|
|
616
769
|
installed: true,
|
|
617
770
|
authenticated: false,
|
|
771
|
+
version: floorGate.check.version ?? void 0,
|
|
772
|
+
skillCapable: false,
|
|
618
773
|
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."
|
|
619
774
|
};
|
|
620
775
|
}
|
|
776
|
+
const authTier = resolveClaudeAuthTier({ env, loggedIn, getStoredKey });
|
|
777
|
+
const remainingMs = AUTH_PROBE_BUDGET_MS - (now() - startedAt);
|
|
778
|
+
const skillCapability = loggedIn === true && remainingMs >= MIN_SKILL_PROBE_MS ? probeSkillCapability({
|
|
779
|
+
versionOutput: probe.stdout,
|
|
780
|
+
env,
|
|
781
|
+
timeoutMs: Math.min(2e3, remainingMs)
|
|
782
|
+
}) : { compatible: false };
|
|
621
783
|
return {
|
|
622
784
|
installed: true,
|
|
623
785
|
authenticated: true,
|
|
786
|
+
version: floorGate.check.version ?? void 0,
|
|
787
|
+
skillCapable: skillCapability.compatible === true,
|
|
624
788
|
// Dispatch-time billing signal, carried on the same probe that already
|
|
625
789
|
// paid for the login read. Never sent for a non-authenticated result:
|
|
626
790
|
// there is no tier without a working credential.
|
|
627
|
-
authTier
|
|
791
|
+
authTier,
|
|
628
792
|
message: loggedIn === true ? "claude CLI installed and logged in (claude auth status)" : "claude binary found (login state unknown \u2014 auth check is best-effort)"
|
|
629
793
|
};
|
|
630
794
|
} catch (error) {
|
|
@@ -636,6 +800,24 @@ async function checkClaudeAuth({
|
|
|
636
800
|
}
|
|
637
801
|
}
|
|
638
802
|
|
|
803
|
+
// ../../scripts/virtual-office/code-runner/agent-auth-attestation.mjs
|
|
804
|
+
var AGENT_AUTH_SOURCE = Object.freeze({
|
|
805
|
+
/** A flat-cost linked account (`claude auth login`). No per-token vendor bill. */
|
|
806
|
+
LOGIN: "login",
|
|
807
|
+
/** A metered per-token credential the vendor bills. */
|
|
808
|
+
API_KEY: "api_key"
|
|
809
|
+
});
|
|
810
|
+
var AGENT_AUTH_SOURCES = Object.freeze([
|
|
811
|
+
AGENT_AUTH_SOURCE.LOGIN,
|
|
812
|
+
AGENT_AUTH_SOURCE.API_KEY
|
|
813
|
+
]);
|
|
814
|
+
var API_BILLING_ENV_NAMES = Object.freeze([
|
|
815
|
+
"ANTHROPIC_API_KEY",
|
|
816
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
817
|
+
"CLAUDE_API_KEY"
|
|
818
|
+
]);
|
|
819
|
+
var API_BILLING_ENV_NAME_SET = new Set(API_BILLING_ENV_NAMES);
|
|
820
|
+
|
|
639
821
|
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
640
822
|
function parseStreamEvent(line) {
|
|
641
823
|
return parseClaudeStreamEvent(line);
|
|
@@ -647,8 +829,8 @@ var ClaudeRunner = class {
|
|
|
647
829
|
get binary() {
|
|
648
830
|
return "claude";
|
|
649
831
|
}
|
|
650
|
-
buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy } = {}) {
|
|
651
|
-
return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy });
|
|
832
|
+
buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema } = {}) {
|
|
833
|
+
return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy, structuredOutputSchema });
|
|
652
834
|
}
|
|
653
835
|
parseEvent(line) {
|
|
654
836
|
return parseStreamEvent(line);
|
|
@@ -672,11 +854,14 @@ var ClaudeRunner = class {
|
|
|
672
854
|
async checkAuth() {
|
|
673
855
|
return checkClaudeAuth();
|
|
674
856
|
}
|
|
857
|
+
checkSkillCapability({ bin = this.binary, env = process.env } = {}) {
|
|
858
|
+
return probeClaudeSkillCapability({ bin, env, freshIdentity: true });
|
|
859
|
+
}
|
|
675
860
|
};
|
|
676
861
|
var claudeRunner = new ClaudeRunner();
|
|
677
862
|
|
|
678
863
|
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
679
|
-
import { spawnSync as
|
|
864
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
680
865
|
import { existsSync as existsSync3 } from "node:fs";
|
|
681
866
|
import { win32 } from "node:path";
|
|
682
867
|
|
|
@@ -928,7 +1113,7 @@ function parseCodexEvent(line) {
|
|
|
928
1113
|
return null;
|
|
929
1114
|
}
|
|
930
1115
|
var CodexRunner = class {
|
|
931
|
-
constructor({ spawn: spawn2 =
|
|
1116
|
+
constructor({ spawn: spawn2 = spawnSync7, resolveBinary = resolveCodexBinary, env = process.env } = {}) {
|
|
932
1117
|
this.spawn = spawn2;
|
|
933
1118
|
this.resolveBinary = resolveBinary;
|
|
934
1119
|
this.env = env;
|
|
@@ -1039,7 +1224,7 @@ ${login.stderr || ""}`.trim();
|
|
|
1039
1224
|
var codexRunner = new CodexRunner();
|
|
1040
1225
|
|
|
1041
1226
|
// ../../scripts/virtual-office/code-runner/cursor-runner.mjs
|
|
1042
|
-
import { spawnSync as
|
|
1227
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
1043
1228
|
function buildCursorArgs({ model, prompt } = {}) {
|
|
1044
1229
|
const args = ["-p", "--output-format", "stream-json", "--force"];
|
|
1045
1230
|
if (model) {
|
|
@@ -1144,7 +1329,7 @@ var CursorRunner = class {
|
|
|
1144
1329
|
/** Best-effort: is `cursor-agent` on PATH? Never throws. */
|
|
1145
1330
|
async checkAuth() {
|
|
1146
1331
|
try {
|
|
1147
|
-
const { status, error } =
|
|
1332
|
+
const { status, error } = spawnSync8("cursor-agent", ["--version"], {
|
|
1148
1333
|
shell: false,
|
|
1149
1334
|
windowsHide: true,
|
|
1150
1335
|
timeout: 3e3,
|
package/dist/cli.js
CHANGED
|
@@ -6295,7 +6295,7 @@ init_common();
|
|
|
6295
6295
|
import { spawn } from "node:child_process";
|
|
6296
6296
|
import { homedir as homedir5 } from "node:os";
|
|
6297
6297
|
import { join as join7 } from "node:path";
|
|
6298
|
-
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
6298
|
+
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4, writeSync } from "node:fs";
|
|
6299
6299
|
|
|
6300
6300
|
// src/swarm/tier-binding.ts
|
|
6301
6301
|
var SWARM_TIERS = Object.freeze([
|
|
@@ -6892,6 +6892,64 @@ function checkSuccessorLiveness(child, logPath, config, deps = {}) {
|
|
|
6892
6892
|
});
|
|
6893
6893
|
}
|
|
6894
6894
|
|
|
6895
|
+
// src/swarm/successor-auth-env.ts
|
|
6896
|
+
var ALLOW_API_BILLING_ENV = "VO_SUCCESSOR_ALLOW_API_BILLING";
|
|
6897
|
+
var ALLOW_API_BILLING_ENV_VALUE = "1";
|
|
6898
|
+
var ALLOW_API_BILLING_INPUT = "allow_api_billing";
|
|
6899
|
+
var AUTH_SOURCE_LOGIN = "claude.ai login";
|
|
6900
|
+
var AUTH_SOURCE_API_KEY = "api key (explicitly allowed)";
|
|
6901
|
+
var NAMED_AUTH_ENV_KEYS = Object.freeze([
|
|
6902
|
+
"ANTHROPIC_API_KEY",
|
|
6903
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
6904
|
+
"CLAUDE_API_KEY"
|
|
6905
|
+
]);
|
|
6906
|
+
function isSuccessorAuthEnvKey(name) {
|
|
6907
|
+
const upper = String(name ?? "").trim().toUpperCase();
|
|
6908
|
+
if (upper.length === 0) return false;
|
|
6909
|
+
if (NAMED_AUTH_ENV_KEYS.includes(upper)) return true;
|
|
6910
|
+
if (!upper.endsWith("_API_KEY")) return false;
|
|
6911
|
+
return upper.startsWith("ANTHROPIC_") || upper.startsWith("CLAUDE_");
|
|
6912
|
+
}
|
|
6913
|
+
function resolveSuccessorAuthEnv(parentEnv, allowApiBillingInput) {
|
|
6914
|
+
const flagRaw = parentEnv[ALLOW_API_BILLING_ENV];
|
|
6915
|
+
const flagSet = typeof flagRaw === "string" && flagRaw.trim() === ALLOW_API_BILLING_ENV_VALUE;
|
|
6916
|
+
const inputAsked = allowApiBillingInput === true;
|
|
6917
|
+
if (flagSet && !inputAsked) {
|
|
6918
|
+
return {
|
|
6919
|
+
ok: false,
|
|
6920
|
+
reason: `${ALLOW_API_BILLING_ENV}=${ALLOW_API_BILLING_ENV_VALUE} is set in this MCP server's environment, but the caller did not pass \`${ALLOW_API_BILLING_INPUT}: true\`. Org API-key billing needs BOTH consents \u2014 an inherited env var on its own is exactly how three headless successors billed ~489M Opus tokens to the org key on 2026-09-06/07. Pass \`${ALLOW_API_BILLING_INPUT}: true\` to bill the API key, or unset ${ALLOW_API_BILLING_ENV} to run the successor on the claude.ai login.`
|
|
6921
|
+
};
|
|
6922
|
+
}
|
|
6923
|
+
if (inputAsked && !flagSet) {
|
|
6924
|
+
return {
|
|
6925
|
+
ok: false,
|
|
6926
|
+
reason: `\`${ALLOW_API_BILLING_INPUT}: true\` was requested, but ${ALLOW_API_BILLING_ENV}=${ALLOW_API_BILLING_ENV_VALUE} is not set in this MCP server's environment. A tool input alone cannot move the payer \u2014 the operator's environment has to consent too. Refusing rather than spawning on the claude.ai login while the caller believes it is on the API key.`
|
|
6927
|
+
};
|
|
6928
|
+
}
|
|
6929
|
+
const env = { ...parentEnv };
|
|
6930
|
+
const present = Object.keys(env).filter((key) => isSuccessorAuthEnvKey(key)).sort();
|
|
6931
|
+
if (flagSet && inputAsked) {
|
|
6932
|
+
return {
|
|
6933
|
+
ok: true,
|
|
6934
|
+
// Honest about what is actually there: an opt-in with no key in the env
|
|
6935
|
+
// still runs on the login, and reporting otherwise would make the receipt
|
|
6936
|
+
// a claim rather than a record.
|
|
6937
|
+
source: present.length > 0 ? AUTH_SOURCE_API_KEY : AUTH_SOURCE_LOGIN,
|
|
6938
|
+
strippedEnvNames: [],
|
|
6939
|
+
preservedEnvNames: present,
|
|
6940
|
+
env
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
for (const key of present) delete env[key];
|
|
6944
|
+
return { ok: true, source: AUTH_SOURCE_LOGIN, strippedEnvNames: present, preservedEnvNames: [], env };
|
|
6945
|
+
}
|
|
6946
|
+
function successorAuthReceiptLine(receipt) {
|
|
6947
|
+
const stripped = receipt.strippedEnvNames.length > 0 ? receipt.strippedEnvNames.join(",") : "none";
|
|
6948
|
+
const note = receipt.source === AUTH_SOURCE_API_KEY ? ` (ORG API KEY BILLING \u2014 ${ALLOW_API_BILLING_ENV}=${ALLOW_API_BILLING_ENV_VALUE} + ${ALLOW_API_BILLING_INPUT}:true)` : "";
|
|
6949
|
+
return `[vo_spawn_successor] ${receipt.nowIso} agent=${receipt.agent} tier=${receipt.tier} auth_source="${receipt.source}" stripped_auth_env=${stripped}${note}
|
|
6950
|
+
`;
|
|
6951
|
+
}
|
|
6952
|
+
|
|
6895
6953
|
// src/tools/session/spawn-successor.ts
|
|
6896
6954
|
var TOOL_NAME21 = "vo_spawn_successor";
|
|
6897
6955
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
@@ -6917,13 +6975,17 @@ var inputSchema21 = {
|
|
|
6917
6975
|
agent: {
|
|
6918
6976
|
type: "string",
|
|
6919
6977
|
description: `Which agent to spawn ('claude' | 'codex'). Normally omitted: the agent comes from the swarm tier binding inherited via ${SWARM_TIER_BINDING_ENV}. When a binding IS inherited this may only RESTATE the bound agent \u2014 an agent that contradicts the binding is REFUSED, because a different agent is a different payer and the payer was decided once, at admission.`
|
|
6978
|
+
},
|
|
6979
|
+
[ALLOW_API_BILLING_INPUT]: {
|
|
6980
|
+
type: "boolean",
|
|
6981
|
+
description: `Bill this successor to the ORG API key instead of the claude.ai login. Default false: the Anthropic/Claude auth env vars are DELETED from the child env. Requires ${ALLOW_API_BILLING_ENV}=1 in the server environment as well \u2014 both consents must agree or the spawn is refused. Incident 2026-09-06/07: three successors that merely INHERITED ANTHROPIC_API_KEY burned ~489M Opus 5 tokens (~$300) and exhausted the org monthly limit.`
|
|
6920
6982
|
}
|
|
6921
6983
|
},
|
|
6922
6984
|
required: [],
|
|
6923
6985
|
additionalProperties: false
|
|
6924
6986
|
};
|
|
6925
6987
|
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
6926
|
-
var description21 =
|
|
6988
|
+
var description21 = `Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless \`claude -p\` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. Verifies the child is actually alive (survives an early-exit window; an optional log-output window is off by default) before reporting success. The successor runs on the operator's claude.ai LOGIN: the ANTHROPIC/CLAUDE auth env vars are deleted from the child env unless ${ALLOW_API_BILLING_ENV}=1 AND \`${ALLOW_API_BILLING_INPUT}: true\` are BOTH present (incident 2026-09-06/07 \u2014 three successors that merely inherited ANTHROPIC_API_KEY billed ~489M Opus 5 tokens, ~$300, to the org key and exhausted its monthly limit). Returns {spawned, pid, log_path, handoff_path, auth_source, stripped_auth_env}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.`;
|
|
6927
6989
|
function isToolInput20(v) {
|
|
6928
6990
|
if (typeof v !== "object" || v === null) return false;
|
|
6929
6991
|
const o = v;
|
|
@@ -6932,6 +6994,7 @@ function isToolInput20(v) {
|
|
|
6932
6994
|
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
6933
6995
|
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
6934
6996
|
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
6997
|
+
if (o[ALLOW_API_BILLING_INPUT] !== void 0 && typeof o[ALLOW_API_BILLING_INPUT] !== "boolean") return false;
|
|
6935
6998
|
return true;
|
|
6936
6999
|
}
|
|
6937
7000
|
function retiredCounterRefusal(v) {
|
|
@@ -7011,6 +7074,21 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
|
|
|
7011
7074
|
}
|
|
7012
7075
|
});
|
|
7013
7076
|
}
|
|
7077
|
+
const envSource = overrides.envSource ?? process.env;
|
|
7078
|
+
const auth = resolveSuccessorAuthEnv(envSource, rawInput[ALLOW_API_BILLING_INPUT]);
|
|
7079
|
+
if (!auth.ok) {
|
|
7080
|
+
return jsonContent({
|
|
7081
|
+
tool: TOOL_NAME21,
|
|
7082
|
+
schema_version: 1,
|
|
7083
|
+
payload: {
|
|
7084
|
+
spawned: false,
|
|
7085
|
+
reason: auth.reason,
|
|
7086
|
+
agent: plan.agent,
|
|
7087
|
+
tier: plan.tier,
|
|
7088
|
+
handoff_path: handoffPath
|
|
7089
|
+
}
|
|
7090
|
+
});
|
|
7091
|
+
}
|
|
7014
7092
|
const platform = overrides.platform ?? process.platform;
|
|
7015
7093
|
let resolvedBin = plan.bin;
|
|
7016
7094
|
if (platform === "win32") {
|
|
@@ -7034,6 +7112,16 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
|
|
|
7034
7112
|
mkdirSync4(logDir, { recursive: true });
|
|
7035
7113
|
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
7036
7114
|
const logFd = openSync2(logPath, "a");
|
|
7115
|
+
writeSync(
|
|
7116
|
+
logFd,
|
|
7117
|
+
successorAuthReceiptLine({
|
|
7118
|
+
nowIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7119
|
+
agent: plan.agent,
|
|
7120
|
+
tier: plan.tier,
|
|
7121
|
+
source: auth.source,
|
|
7122
|
+
strippedEnvNames: auth.strippedEnvNames
|
|
7123
|
+
})
|
|
7124
|
+
);
|
|
7037
7125
|
const child = spawnImpl(resolvedBin, [...plan.args], {
|
|
7038
7126
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
7039
7127
|
detached: true,
|
|
@@ -7045,10 +7133,16 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
|
|
|
7045
7133
|
shell: false,
|
|
7046
7134
|
windowsHide: true,
|
|
7047
7135
|
windowsVerbatimArguments: false,
|
|
7048
|
-
//
|
|
7049
|
-
//
|
|
7050
|
-
//
|
|
7051
|
-
|
|
7136
|
+
// ALWAYS an explicit env (2026-09-10). This used to be
|
|
7137
|
+
// `...(plan.bound ? { env: { ...process.env, ...plan.env } } : {})`, so an
|
|
7138
|
+
// UNBOUND spawn — the ordinary case, since `agent`/bindings are "normally
|
|
7139
|
+
// omitted" — passed no `env` key at all and the child inherited the whole
|
|
7140
|
+
// parent environment, ANTHROPIC_API_KEY included. `auth.env` is that same
|
|
7141
|
+
// environment with the Anthropic/Claude auth family removed (or kept, under
|
|
7142
|
+
// the two-consent opt-in). The tier binding is layered ON TOP, unchanged:
|
|
7143
|
+
// without it the successor inherits no tier and re-resolves its own, which
|
|
7144
|
+
// is the split-payer defect one generation down.
|
|
7145
|
+
env: { ...auth.env, ...plan.bound ? plan.env : {} }
|
|
7052
7146
|
});
|
|
7053
7147
|
closeSync2(logFd);
|
|
7054
7148
|
let spawnError = null;
|
|
@@ -7090,7 +7184,11 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
|
|
|
7090
7184
|
log_path: logPath,
|
|
7091
7185
|
agent: plan.agent,
|
|
7092
7186
|
tier: plan.tier,
|
|
7093
|
-
handoff_path: handoffPath
|
|
7187
|
+
handoff_path: handoffPath,
|
|
7188
|
+
// Surfaced on the failure path too: a successor that died still spent
|
|
7189
|
+
// whatever it spent, and reconciling that needs the payer.
|
|
7190
|
+
auth_source: auth.source,
|
|
7191
|
+
stripped_auth_env: auth.strippedEnvNames
|
|
7094
7192
|
}
|
|
7095
7193
|
});
|
|
7096
7194
|
}
|
|
@@ -7112,7 +7210,11 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
|
|
|
7112
7210
|
ledger_cap_remaining_usd: plan.capRemainingUsd,
|
|
7113
7211
|
// Additive (2026-08-17): true only once the child survived the
|
|
7114
7212
|
// early-exit window (and the output window, when that gate is enabled).
|
|
7115
|
-
verified_alive: true
|
|
7213
|
+
verified_alive: true,
|
|
7214
|
+
// Additive (2026-09-10): WHO PAYS for this successor, and the auth env
|
|
7215
|
+
// names removed to make that true. Names only — never key material.
|
|
7216
|
+
auth_source: auth.source,
|
|
7217
|
+
stripped_auth_env: auth.strippedEnvNames
|
|
7116
7218
|
}
|
|
7117
7219
|
});
|
|
7118
7220
|
}
|