@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/index.js CHANGED
@@ -5274,7 +5274,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
5274
5274
  import { spawn } from "node:child_process";
5275
5275
  import { homedir as homedir5 } from "node:os";
5276
5276
  import { join as join7 } from "node:path";
5277
- 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";
5277
+ 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";
5278
5278
 
5279
5279
  // src/swarm/tier-binding.ts
5280
5280
  var SWARM_TIERS = Object.freeze([
@@ -5871,6 +5871,64 @@ function checkSuccessorLiveness(child, logPath, config, deps = {}) {
5871
5871
  });
5872
5872
  }
5873
5873
 
5874
+ // src/swarm/successor-auth-env.ts
5875
+ var ALLOW_API_BILLING_ENV = "VO_SUCCESSOR_ALLOW_API_BILLING";
5876
+ var ALLOW_API_BILLING_ENV_VALUE = "1";
5877
+ var ALLOW_API_BILLING_INPUT = "allow_api_billing";
5878
+ var AUTH_SOURCE_LOGIN = "claude.ai login";
5879
+ var AUTH_SOURCE_API_KEY = "api key (explicitly allowed)";
5880
+ var NAMED_AUTH_ENV_KEYS = Object.freeze([
5881
+ "ANTHROPIC_API_KEY",
5882
+ "ANTHROPIC_AUTH_TOKEN",
5883
+ "CLAUDE_API_KEY"
5884
+ ]);
5885
+ function isSuccessorAuthEnvKey(name) {
5886
+ const upper = String(name ?? "").trim().toUpperCase();
5887
+ if (upper.length === 0) return false;
5888
+ if (NAMED_AUTH_ENV_KEYS.includes(upper)) return true;
5889
+ if (!upper.endsWith("_API_KEY")) return false;
5890
+ return upper.startsWith("ANTHROPIC_") || upper.startsWith("CLAUDE_");
5891
+ }
5892
+ function resolveSuccessorAuthEnv(parentEnv, allowApiBillingInput) {
5893
+ const flagRaw = parentEnv[ALLOW_API_BILLING_ENV];
5894
+ const flagSet = typeof flagRaw === "string" && flagRaw.trim() === ALLOW_API_BILLING_ENV_VALUE;
5895
+ const inputAsked = allowApiBillingInput === true;
5896
+ if (flagSet && !inputAsked) {
5897
+ return {
5898
+ ok: false,
5899
+ 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.`
5900
+ };
5901
+ }
5902
+ if (inputAsked && !flagSet) {
5903
+ return {
5904
+ ok: false,
5905
+ 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.`
5906
+ };
5907
+ }
5908
+ const env = { ...parentEnv };
5909
+ const present = Object.keys(env).filter((key) => isSuccessorAuthEnvKey(key)).sort();
5910
+ if (flagSet && inputAsked) {
5911
+ return {
5912
+ ok: true,
5913
+ // Honest about what is actually there: an opt-in with no key in the env
5914
+ // still runs on the login, and reporting otherwise would make the receipt
5915
+ // a claim rather than a record.
5916
+ source: present.length > 0 ? AUTH_SOURCE_API_KEY : AUTH_SOURCE_LOGIN,
5917
+ strippedEnvNames: [],
5918
+ preservedEnvNames: present,
5919
+ env
5920
+ };
5921
+ }
5922
+ for (const key of present) delete env[key];
5923
+ return { ok: true, source: AUTH_SOURCE_LOGIN, strippedEnvNames: present, preservedEnvNames: [], env };
5924
+ }
5925
+ function successorAuthReceiptLine(receipt) {
5926
+ const stripped = receipt.strippedEnvNames.length > 0 ? receipt.strippedEnvNames.join(",") : "none";
5927
+ 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)` : "";
5928
+ return `[vo_spawn_successor] ${receipt.nowIso} agent=${receipt.agent} tier=${receipt.tier} auth_source="${receipt.source}" stripped_auth_env=${stripped}${note}
5929
+ `;
5930
+ }
5931
+
5874
5932
  // src/tools/session/spawn-successor.ts
5875
5933
  var TOOL_NAME21 = "vo_spawn_successor";
5876
5934
  var MAX_HANDOFF_BYTES = 64e3;
@@ -5896,13 +5954,17 @@ var inputSchema21 = {
5896
5954
  agent: {
5897
5955
  type: "string",
5898
5956
  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.`
5957
+ },
5958
+ [ALLOW_API_BILLING_INPUT]: {
5959
+ type: "boolean",
5960
+ 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.`
5899
5961
  }
5900
5962
  },
5901
5963
  required: [],
5902
5964
  additionalProperties: false
5903
5965
  };
5904
5966
  var RETIRED_COUNTER_INPUT = "spawns_so_far";
5905
- 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. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
5967
+ 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.`;
5906
5968
  function isToolInput20(v) {
5907
5969
  if (typeof v !== "object" || v === null) return false;
5908
5970
  const o = v;
@@ -5911,6 +5973,7 @@ function isToolInput20(v) {
5911
5973
  if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5912
5974
  if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
5913
5975
  if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
5976
+ if (o[ALLOW_API_BILLING_INPUT] !== void 0 && typeof o[ALLOW_API_BILLING_INPUT] !== "boolean") return false;
5914
5977
  return true;
5915
5978
  }
5916
5979
  function retiredCounterRefusal(v) {
@@ -5990,6 +6053,21 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
5990
6053
  }
5991
6054
  });
5992
6055
  }
6056
+ const envSource = overrides.envSource ?? process.env;
6057
+ const auth = resolveSuccessorAuthEnv(envSource, rawInput[ALLOW_API_BILLING_INPUT]);
6058
+ if (!auth.ok) {
6059
+ return jsonContent({
6060
+ tool: TOOL_NAME21,
6061
+ schema_version: 1,
6062
+ payload: {
6063
+ spawned: false,
6064
+ reason: auth.reason,
6065
+ agent: plan.agent,
6066
+ tier: plan.tier,
6067
+ handoff_path: handoffPath
6068
+ }
6069
+ });
6070
+ }
5993
6071
  const platform = overrides.platform ?? process.platform;
5994
6072
  let resolvedBin = plan.bin;
5995
6073
  if (platform === "win32") {
@@ -6013,6 +6091,16 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
6013
6091
  mkdirSync4(logDir, { recursive: true });
6014
6092
  const logPath = join7(logDir, `successor-${Date.now()}.log`);
6015
6093
  const logFd = openSync2(logPath, "a");
6094
+ writeSync(
6095
+ logFd,
6096
+ successorAuthReceiptLine({
6097
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
6098
+ agent: plan.agent,
6099
+ tier: plan.tier,
6100
+ source: auth.source,
6101
+ strippedEnvNames: auth.strippedEnvNames
6102
+ })
6103
+ );
6016
6104
  const child = spawnImpl(resolvedBin, [...plan.args], {
6017
6105
  cwd: rawInput.cwd?.trim() || process.cwd(),
6018
6106
  detached: true,
@@ -6024,10 +6112,16 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
6024
6112
  shell: false,
6025
6113
  windowsHide: true,
6026
6114
  windowsVerbatimArguments: false,
6027
- // Carry the SAME binding to the child. Without this the successor inherits
6028
- // no tier and re-resolves its own which is the split-payer defect one
6029
- // generation down.
6030
- ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
6115
+ // ALWAYS an explicit env (2026-09-10). This used to be
6116
+ // `...(plan.bound ? { env: { ...process.env, ...plan.env } } : {})`, so an
6117
+ // UNBOUND spawn — the ordinary case, since `agent`/bindings are "normally
6118
+ // omitted" passed no `env` key at all and the child inherited the whole
6119
+ // parent environment, ANTHROPIC_API_KEY included. `auth.env` is that same
6120
+ // environment with the Anthropic/Claude auth family removed (or kept, under
6121
+ // the two-consent opt-in). The tier binding is layered ON TOP, unchanged:
6122
+ // without it the successor inherits no tier and re-resolves its own, which
6123
+ // is the split-payer defect one generation down.
6124
+ env: { ...auth.env, ...plan.bound ? plan.env : {} }
6031
6125
  });
6032
6126
  closeSync2(logFd);
6033
6127
  let spawnError = null;
@@ -6069,7 +6163,11 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
6069
6163
  log_path: logPath,
6070
6164
  agent: plan.agent,
6071
6165
  tier: plan.tier,
6072
- handoff_path: handoffPath
6166
+ handoff_path: handoffPath,
6167
+ // Surfaced on the failure path too: a successor that died still spent
6168
+ // whatever it spent, and reconciling that needs the payer.
6169
+ auth_source: auth.source,
6170
+ stripped_auth_env: auth.strippedEnvNames
6073
6171
  }
6074
6172
  });
6075
6173
  }
@@ -6091,7 +6189,11 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn,
6091
6189
  ledger_cap_remaining_usd: plan.capRemainingUsd,
6092
6190
  // Additive (2026-08-17): true only once the child survived the
6093
6191
  // early-exit window (and the output window, when that gate is enabled).
6094
- verified_alive: true
6192
+ verified_alive: true,
6193
+ // Additive (2026-09-10): WHO PAYS for this successor, and the auth env
6194
+ // names removed to make that true. Names only — never key material.
6195
+ auth_source: auth.source,
6196
+ stripped_auth_env: auth.strippedEnvNames
6095
6197
  }
6096
6198
  });
6097
6199
  }