@algosuite/vo-mcp 0.2.0-beta.72 → 0.2.0-beta.74

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.
@@ -800,6 +800,24 @@ async function checkClaudeAuth({
800
800
  }
801
801
  }
802
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
+
803
821
  // ../../scripts/virtual-office/code-runner/claude-runner.mjs
804
822
  function parseStreamEvent(line) {
805
823
  return parseClaudeStreamEvent(line);
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 = "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.";
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
- // Carry the SAME binding to the child. Without this the successor inherits
7049
- // no tier and re-resolves its own which is the split-payer defect one
7050
- // generation down.
7051
- ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
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
  }