@algosuite/vo-mcp 0.2.0-beta.28 → 0.2.0-beta.29
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 +65 -35
- package/dist/cli.js +476 -42
- package/dist/cli.js.map +4 -4
- package/dist/index.js +467 -33
- package/dist/index.js.map +4 -4
- package/dist/runner-cli.js +247 -60
- package/dist/runner-cli.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -324,7 +324,7 @@ __export(memory_knowledge_bridge_exports, {
|
|
|
324
324
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
325
325
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
326
326
|
});
|
|
327
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
327
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
328
328
|
function extractMemoryTitle(fileName, content) {
|
|
329
329
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
330
330
|
if (frontmatter) {
|
|
@@ -357,7 +357,7 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
357
357
|
const failures = [];
|
|
358
358
|
for (const fileName of files) {
|
|
359
359
|
try {
|
|
360
|
-
const content =
|
|
360
|
+
const content = readFileSync8(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
361
361
|
if (content.length > CONTENT_HARD_LIMIT) {
|
|
362
362
|
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
363
363
|
continue;
|
|
@@ -4843,9 +4843,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
4843
4843
|
|
|
4844
4844
|
// src/tools/session/spawn-successor.ts
|
|
4845
4845
|
import { spawn } from "node:child_process";
|
|
4846
|
+
import { homedir as homedir5 } from "node:os";
|
|
4847
|
+
import { join as join7 } from "node:path";
|
|
4848
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
4849
|
+
|
|
4850
|
+
// src/swarm/tier-binding.ts
|
|
4851
|
+
var SWARM_TIERS = Object.freeze([
|
|
4852
|
+
"tier1_subscription",
|
|
4853
|
+
"tier1_local",
|
|
4854
|
+
"tier2_user_key",
|
|
4855
|
+
"tier3_platform_key",
|
|
4856
|
+
"refused",
|
|
4857
|
+
"unresolved"
|
|
4858
|
+
]);
|
|
4859
|
+
var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
|
|
4860
|
+
"tier1_subscription",
|
|
4861
|
+
"tier1_local",
|
|
4862
|
+
"tier2_user_key",
|
|
4863
|
+
"tier3_platform_key"
|
|
4864
|
+
]);
|
|
4865
|
+
var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
|
|
4866
|
+
var MAX_BOUND_SUBAGENTS = 20;
|
|
4867
|
+
function isPositiveCap(cap) {
|
|
4868
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
|
|
4869
|
+
}
|
|
4870
|
+
function unresolvedBinding(swarmId, nowIso, reason) {
|
|
4871
|
+
return {
|
|
4872
|
+
schema_version: 1,
|
|
4873
|
+
swarm_id: swarmId,
|
|
4874
|
+
tier: "unresolved",
|
|
4875
|
+
agent: null,
|
|
4876
|
+
reason,
|
|
4877
|
+
exhausted_agents: [],
|
|
4878
|
+
subagent_budget: 0,
|
|
4879
|
+
spend_cap_usd: null,
|
|
4880
|
+
resolved_at: nowIso
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4883
|
+
function serializeSwarmTierBinding(binding) {
|
|
4884
|
+
return JSON.stringify(binding);
|
|
4885
|
+
}
|
|
4886
|
+
function parseSwarmTierBinding(raw, nowIso) {
|
|
4887
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
4888
|
+
return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
|
|
4889
|
+
}
|
|
4890
|
+
let parsed;
|
|
4891
|
+
try {
|
|
4892
|
+
parsed = JSON.parse(raw);
|
|
4893
|
+
} catch {
|
|
4894
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
|
|
4895
|
+
}
|
|
4896
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4897
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
|
|
4898
|
+
}
|
|
4899
|
+
const o = parsed;
|
|
4900
|
+
const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
|
|
4901
|
+
if (o["schema_version"] !== 1) {
|
|
4902
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
|
|
4903
|
+
}
|
|
4904
|
+
const tier = o["tier"];
|
|
4905
|
+
if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
|
|
4906
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
|
|
4907
|
+
}
|
|
4908
|
+
const budget = o["subagent_budget"];
|
|
4909
|
+
const cap = o["spend_cap_usd"];
|
|
4910
|
+
const capNum = isPositiveCap(cap) ? cap : null;
|
|
4911
|
+
if (tier === "tier3_platform_key" && capNum === null) {
|
|
4912
|
+
return unresolvedBinding(
|
|
4913
|
+
swarmId,
|
|
4914
|
+
nowIso,
|
|
4915
|
+
"inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
|
|
4916
|
+
);
|
|
4917
|
+
}
|
|
4918
|
+
return {
|
|
4919
|
+
schema_version: 1,
|
|
4920
|
+
swarm_id: swarmId,
|
|
4921
|
+
tier,
|
|
4922
|
+
agent: typeof o["agent"] === "string" ? o["agent"] : null,
|
|
4923
|
+
reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
|
|
4924
|
+
exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
|
|
4925
|
+
subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
|
|
4926
|
+
spend_cap_usd: capNum,
|
|
4927
|
+
resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
function inheritSwarmTierBinding(env, nowIso) {
|
|
4931
|
+
return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
|
|
4932
|
+
}
|
|
4933
|
+
function bindingEnvFragment(binding) {
|
|
4934
|
+
return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
|
|
4935
|
+
}
|
|
4936
|
+
function childBindingEnvFragment(binding, allocatedCapUsd = null) {
|
|
4937
|
+
return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
|
|
4938
|
+
}
|
|
4939
|
+
function admitSubagentSpawn(binding, spawnsSoFar = 0) {
|
|
4940
|
+
if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
|
|
4941
|
+
return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
|
|
4942
|
+
}
|
|
4943
|
+
if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
|
|
4944
|
+
return {
|
|
4945
|
+
allowed: false,
|
|
4946
|
+
reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
4949
|
+
if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
|
|
4950
|
+
return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
|
|
4951
|
+
}
|
|
4952
|
+
if (spawnsSoFar >= binding.subagent_budget) {
|
|
4953
|
+
return {
|
|
4954
|
+
allowed: false,
|
|
4955
|
+
reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
|
|
4959
|
+
}
|
|
4960
|
+
function childBinding(binding, allocatedCapUsd = null) {
|
|
4961
|
+
const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
|
|
4962
|
+
const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
|
|
4963
|
+
return {
|
|
4964
|
+
...binding,
|
|
4965
|
+
subagent_budget: Math.max(0, binding.subagent_budget - 1),
|
|
4966
|
+
// A child never carries more than its parent, whatever the ledger says: a
|
|
4967
|
+
// forged or hand-edited pool cannot inflate a descendant above the binding
|
|
4968
|
+
// it descends from.
|
|
4969
|
+
spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4972
|
+
function agentBindingRefusal(binding, requestedAgent) {
|
|
4973
|
+
const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
|
|
4974
|
+
if (requested.length === 0) return null;
|
|
4975
|
+
if (binding.agent !== null && requested === binding.agent) return null;
|
|
4976
|
+
return `swarm '${binding.swarm_id}' is bound to agent '${binding.agent ?? "none"}' under tier '${binding.tier}'; a caller-supplied agent '${requested}' would move this fan-out onto a different payer \u2014 refusing (the tier is decided once, at admission, and an inherited binding cannot be renegotiated)`;
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
// src/swarm/successor-launch.ts
|
|
4980
|
+
var AGENT_LAUNCH_SHAPES = Object.freeze({
|
|
4981
|
+
claude: {
|
|
4982
|
+
bin: "claude",
|
|
4983
|
+
baseArgs: ["-p", "--permission-mode", "acceptEdits"],
|
|
4984
|
+
enforcesMaxTurns: true,
|
|
4985
|
+
maxTurnsFlag: "--max-turns",
|
|
4986
|
+
windowsShellSafe: true
|
|
4987
|
+
},
|
|
4988
|
+
codex: {
|
|
4989
|
+
bin: "codex",
|
|
4990
|
+
baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
4991
|
+
enforcesMaxTurns: false,
|
|
4992
|
+
// `-` makes codex read the prompt from stdin (injection-safe), matching how
|
|
4993
|
+
// codex-runner.mjs already spawns it.
|
|
4994
|
+
trailingArgs: ["-"],
|
|
4995
|
+
// `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
|
|
4996
|
+
// unverified, so win32 refuses rather than risking a mangled sandbox flag.
|
|
4997
|
+
windowsShellSafe: false
|
|
4998
|
+
}
|
|
4999
|
+
});
|
|
5000
|
+
function resolveSuccessorLaunch(input) {
|
|
5001
|
+
const agent = typeof input.agent === "string" ? input.agent.trim() : "";
|
|
5002
|
+
if (!agent) {
|
|
5003
|
+
return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
|
|
5004
|
+
}
|
|
5005
|
+
const shape = AGENT_LAUNCH_SHAPES[agent];
|
|
5006
|
+
if (!shape) {
|
|
5007
|
+
const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
|
|
5008
|
+
return {
|
|
5009
|
+
ok: false,
|
|
5010
|
+
reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
|
|
5014
|
+
if (wantsMaxTurns && !shape.enforcesMaxTurns) {
|
|
5015
|
+
return {
|
|
5016
|
+
ok: false,
|
|
5017
|
+
reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
const platform = input.platform ?? process.platform;
|
|
5021
|
+
if (platform === "win32" && !shape.windowsShellSafe) {
|
|
5022
|
+
return {
|
|
5023
|
+
ok: false,
|
|
5024
|
+
reason: `agent '${agent}' has an argv whose behaviour under Windows cmd.exe re-parsing is unverified \u2014 refusing rather than emitting a command line that may mean something else`
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
const args = [...shape.baseArgs];
|
|
5028
|
+
if (wantsMaxTurns && shape.maxTurnsFlag) {
|
|
5029
|
+
args.push(shape.maxTurnsFlag, String(input.maxTurns));
|
|
5030
|
+
}
|
|
5031
|
+
if (shape.trailingArgs) args.push(...shape.trailingArgs);
|
|
5032
|
+
return { ok: true, agent, bin: shape.bin, args };
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
// src/swarm/spawn-ledger.ts
|
|
5036
|
+
import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4846
5037
|
import { homedir as homedir4 } from "node:os";
|
|
4847
5038
|
import { join as join6 } from "node:path";
|
|
4848
|
-
|
|
5039
|
+
var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
|
|
5040
|
+
function resolveLedgerDir(env) {
|
|
5041
|
+
const override = env[SWARM_LEDGER_DIR_ENV];
|
|
5042
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
5043
|
+
return join6(homedir4(), ".vo", "swarm-ledger");
|
|
5044
|
+
}
|
|
5045
|
+
function sanitizeSwarmId(raw) {
|
|
5046
|
+
if (typeof raw !== "string") return null;
|
|
5047
|
+
const id = raw.trim();
|
|
5048
|
+
if (id.length === 0 || id.length > 128) return null;
|
|
5049
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
|
|
5050
|
+
if (id === "." || id === "..") return null;
|
|
5051
|
+
return id;
|
|
5052
|
+
}
|
|
5053
|
+
var CEILING_FILE = "ceiling.json";
|
|
5054
|
+
function createExclusive(path3, contents) {
|
|
5055
|
+
let fd;
|
|
5056
|
+
try {
|
|
5057
|
+
fd = openSync(path3, "wx");
|
|
5058
|
+
} catch {
|
|
5059
|
+
return false;
|
|
5060
|
+
}
|
|
5061
|
+
try {
|
|
5062
|
+
writeFileSync3(fd, contents, "utf8");
|
|
5063
|
+
} finally {
|
|
5064
|
+
closeSync(fd);
|
|
5065
|
+
}
|
|
5066
|
+
return true;
|
|
5067
|
+
}
|
|
5068
|
+
function capToCents(cap) {
|
|
5069
|
+
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
5070
|
+
}
|
|
5071
|
+
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
5072
|
+
const path3 = join6(swarmDir, CEILING_FILE);
|
|
5073
|
+
const head = JSON.stringify({
|
|
5074
|
+
ceiling: proposedCeiling,
|
|
5075
|
+
cap_cents: proposedCapCents,
|
|
5076
|
+
recorded_at: nowIso
|
|
5077
|
+
});
|
|
5078
|
+
if (createExclusive(path3, head)) {
|
|
5079
|
+
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
5080
|
+
}
|
|
5081
|
+
let parsed;
|
|
5082
|
+
try {
|
|
5083
|
+
parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
5084
|
+
} catch {
|
|
5085
|
+
return null;
|
|
5086
|
+
}
|
|
5087
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5088
|
+
const record = parsed;
|
|
5089
|
+
const recorded = record["ceiling"];
|
|
5090
|
+
if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
|
|
5091
|
+
const recordedCap = record["cap_cents"];
|
|
5092
|
+
const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
|
|
5093
|
+
return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
|
|
5094
|
+
}
|
|
5095
|
+
var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
|
|
5096
|
+
const id = sanitizeSwarmId(swarmId);
|
|
5097
|
+
if (id === null) {
|
|
5098
|
+
return {
|
|
5099
|
+
ok: false,
|
|
5100
|
+
reason: `swarm id ${JSON.stringify(swarmId)} is absent or unusable as a ledger key \u2014 refusing a spawn that cannot be counted against a fan-out ceiling`
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
|
|
5104
|
+
if (proposed < 1) {
|
|
5105
|
+
return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
|
|
5106
|
+
}
|
|
5107
|
+
const swarmDir = join6(dir, id);
|
|
5108
|
+
try {
|
|
5109
|
+
mkdirSync3(swarmDir, { recursive: true });
|
|
5110
|
+
} catch (err) {
|
|
5111
|
+
return {
|
|
5112
|
+
ok: false,
|
|
5113
|
+
reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
|
|
5114
|
+
};
|
|
5115
|
+
}
|
|
5116
|
+
const wantedCents = capToCents(proposedCapUsd);
|
|
5117
|
+
const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
|
|
5118
|
+
if (head === null) {
|
|
5119
|
+
return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
|
|
5120
|
+
}
|
|
5121
|
+
const { ceiling, capCents } = head;
|
|
5122
|
+
const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
|
|
5123
|
+
if (wantedCents > 0 && shareCents < 1) {
|
|
5124
|
+
return {
|
|
5125
|
+
ok: false,
|
|
5126
|
+
reason: `swarm '${id}' has no spend allowance left to debit (recorded pool $${(capCents / 100).toFixed(2)} across a ceiling of ${ceiling} leaves under one cent per spawn) \u2014 refusing a platform-billed spawn it cannot fund`
|
|
5127
|
+
};
|
|
5128
|
+
}
|
|
5129
|
+
for (let slot = 0; slot < ceiling; slot++) {
|
|
5130
|
+
const debitedCents = shareCents;
|
|
5131
|
+
const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
|
|
5132
|
+
const claimed = createExclusive(
|
|
5133
|
+
join6(swarmDir, `slot-${slot}.json`),
|
|
5134
|
+
JSON.stringify({
|
|
5135
|
+
slot,
|
|
5136
|
+
ceiling,
|
|
5137
|
+
pid: process.pid,
|
|
5138
|
+
claimed_at: nowIso,
|
|
5139
|
+
// The debit record. Durable and atomic with the claim: this file is
|
|
5140
|
+
// created with O_EXCL, so exactly one claimant ever writes this line.
|
|
5141
|
+
cap_cents_pool: capCents,
|
|
5142
|
+
cap_cents_debited: debitedCents,
|
|
5143
|
+
cap_cents_remaining: remainingCents
|
|
5144
|
+
})
|
|
5145
|
+
);
|
|
5146
|
+
if (claimed) {
|
|
5147
|
+
return {
|
|
5148
|
+
ok: true,
|
|
5149
|
+
slot,
|
|
5150
|
+
ceiling,
|
|
5151
|
+
remaining: ceiling - slot - 1,
|
|
5152
|
+
capUsd: debitedCents > 0 ? debitedCents / 100 : null,
|
|
5153
|
+
capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
|
|
5154
|
+
};
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
return {
|
|
5158
|
+
ok: false,
|
|
5159
|
+
reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
|
|
5160
|
+
};
|
|
5161
|
+
};
|
|
5162
|
+
|
|
5163
|
+
// src/tools/session/spawn-successor.ts
|
|
4849
5164
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
4850
5165
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
4851
5166
|
var inputSchema20 = {
|
|
@@ -4866,11 +5181,16 @@ var inputSchema20 = {
|
|
|
4866
5181
|
max_turns: {
|
|
4867
5182
|
type: "number",
|
|
4868
5183
|
description: "Optional --max-turns bound for the successor."
|
|
5184
|
+
},
|
|
5185
|
+
agent: {
|
|
5186
|
+
type: "string",
|
|
5187
|
+
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.`
|
|
4869
5188
|
}
|
|
4870
5189
|
},
|
|
4871
5190
|
required: [],
|
|
4872
5191
|
additionalProperties: false
|
|
4873
5192
|
};
|
|
5193
|
+
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
4874
5194
|
var description20 = "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/. 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.";
|
|
4875
5195
|
function isToolInput20(v) {
|
|
4876
5196
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -4879,12 +5199,18 @@ function isToolInput20(v) {
|
|
|
4879
5199
|
if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
|
|
4880
5200
|
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
4881
5201
|
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
5202
|
+
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
4882
5203
|
return true;
|
|
4883
5204
|
}
|
|
4884
|
-
function
|
|
5205
|
+
function retiredCounterRefusal(v) {
|
|
5206
|
+
if (typeof v !== "object" || v === null) return null;
|
|
5207
|
+
if (!(RETIRED_COUNTER_INPUT in v)) return null;
|
|
5208
|
+
return `\`${RETIRED_COUNTER_INPUT}\` is no longer accepted: a spawn counter supplied by the process being bounded bounds nothing, and an absent one read as zero. The fan-out ceiling is now enforced by the durable per-swarm spawn ledger; remove the field.`;
|
|
5209
|
+
}
|
|
5210
|
+
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
4885
5211
|
try {
|
|
4886
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(
|
|
4887
|
-
return entries.length > 0 && entries[0] ?
|
|
5212
|
+
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
|
|
5213
|
+
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
4888
5214
|
} catch {
|
|
4889
5215
|
return null;
|
|
4890
5216
|
}
|
|
@@ -4928,9 +5254,87 @@ function buildSuccessorArgs(maxTurns) {
|
|
|
4928
5254
|
}
|
|
4929
5255
|
return args;
|
|
4930
5256
|
}
|
|
5257
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
5258
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
5259
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
5260
|
+
if (!hasBinding) {
|
|
5261
|
+
const explicit = input.agent?.trim();
|
|
5262
|
+
if (explicit) {
|
|
5263
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
5264
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
5265
|
+
return {
|
|
5266
|
+
ok: true,
|
|
5267
|
+
bin: resolved2.bin,
|
|
5268
|
+
args: resolved2.args,
|
|
5269
|
+
agent: resolved2.agent,
|
|
5270
|
+
tier: "unbound",
|
|
5271
|
+
bound: false,
|
|
5272
|
+
env: {},
|
|
5273
|
+
slot: null,
|
|
5274
|
+
capUsd: null,
|
|
5275
|
+
capRemainingUsd: null
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
ok: true,
|
|
5280
|
+
bin: "claude",
|
|
5281
|
+
args: buildSuccessorArgs(input.max_turns),
|
|
5282
|
+
agent: "claude",
|
|
5283
|
+
tier: "unbound",
|
|
5284
|
+
bound: false,
|
|
5285
|
+
env: {},
|
|
5286
|
+
slot: null,
|
|
5287
|
+
capUsd: null,
|
|
5288
|
+
capRemainingUsd: null
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
5292
|
+
const admission = admitSubagentSpawn(binding);
|
|
5293
|
+
if (!admission.allowed) {
|
|
5294
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
5295
|
+
}
|
|
5296
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
5297
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
5298
|
+
const resolved = resolveSuccessorLaunch({
|
|
5299
|
+
agent: binding.agent,
|
|
5300
|
+
maxTurns: input.max_turns,
|
|
5301
|
+
platform
|
|
5302
|
+
});
|
|
5303
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
5304
|
+
const slot = claim({
|
|
5305
|
+
swarmId: binding.swarm_id,
|
|
5306
|
+
proposedCeiling: binding.subagent_budget,
|
|
5307
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
5308
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
5309
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
5310
|
+
dir: resolveLedgerDir(env),
|
|
5311
|
+
nowIso
|
|
5312
|
+
});
|
|
5313
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
5314
|
+
return {
|
|
5315
|
+
ok: true,
|
|
5316
|
+
bin: resolved.bin,
|
|
5317
|
+
args: resolved.args,
|
|
5318
|
+
agent: resolved.agent,
|
|
5319
|
+
tier: binding.tier,
|
|
5320
|
+
bound: true,
|
|
5321
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
5322
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
5323
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
5324
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
5325
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
5326
|
+
// untouched $50 and walked away with $75 between them.
|
|
5327
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
5328
|
+
slot: slot.slot,
|
|
5329
|
+
capUsd: slot.capUsd,
|
|
5330
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
4931
5333
|
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
5334
|
+
const retired = retiredCounterRefusal(rawInput);
|
|
5335
|
+
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
4932
5336
|
if (!isToolInput20(rawInput)) {
|
|
4933
|
-
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
|
|
5337
|
+
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
4934
5338
|
}
|
|
4935
5339
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
4936
5340
|
if (!handoffPath || !existsSync4(handoffPath)) {
|
|
@@ -4943,20 +5347,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
4943
5347
|
}
|
|
4944
5348
|
});
|
|
4945
5349
|
}
|
|
4946
|
-
const handoff =
|
|
5350
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
4947
5351
|
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
4948
|
-
const
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5352
|
+
const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
|
|
5353
|
+
if (!plan.ok) {
|
|
5354
|
+
return jsonContent({
|
|
5355
|
+
tool: TOOL_NAME20,
|
|
5356
|
+
schema_version: 1,
|
|
5357
|
+
payload: {
|
|
5358
|
+
spawned: false,
|
|
5359
|
+
reason: `swarm tier binding refused this spawn: ${plan.reason}`,
|
|
5360
|
+
tier: plan.tier,
|
|
5361
|
+
handoff_path: handoffPath
|
|
5362
|
+
}
|
|
5363
|
+
});
|
|
5364
|
+
}
|
|
5365
|
+
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
5366
|
+
mkdirSync4(logDir, { recursive: true });
|
|
5367
|
+
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
5368
|
+
const logFd = openSync2(logPath, "a");
|
|
5369
|
+
const child = spawnImpl(plan.bin, [...plan.args], {
|
|
4953
5370
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
4954
5371
|
detached: true,
|
|
4955
5372
|
stdio: ["pipe", logFd, logFd],
|
|
4956
|
-
// Windows:
|
|
4957
|
-
// goes via STDIN below, never argv, so the shell never sees it.
|
|
5373
|
+
// Windows: the agent CLIs are .cmd shims — they need a shell to resolve.
|
|
5374
|
+
// The prompt goes via STDIN below, never argv, so the shell never sees it.
|
|
4958
5375
|
shell: process.platform === "win32",
|
|
4959
|
-
windowsHide: true
|
|
5376
|
+
windowsHide: true,
|
|
5377
|
+
// Carry the SAME binding to the child. Without this the successor inherits
|
|
5378
|
+
// no tier and re-resolves its own — which is the split-payer defect one
|
|
5379
|
+
// generation down.
|
|
5380
|
+
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
4960
5381
|
});
|
|
4961
5382
|
let spawnError = null;
|
|
4962
5383
|
child.on("error", (e) => {
|
|
@@ -4972,7 +5393,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
4972
5393
|
return jsonContent({
|
|
4973
5394
|
tool: TOOL_NAME20,
|
|
4974
5395
|
schema_version: 1,
|
|
4975
|
-
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`,
|
|
5396
|
+
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
|
|
5397
|
+
spawned: true,
|
|
5398
|
+
pid: child.pid ?? null,
|
|
5399
|
+
log_path: logPath,
|
|
5400
|
+
handoff_path: handoffPath,
|
|
5401
|
+
agent: plan.agent,
|
|
5402
|
+
tier: plan.tier,
|
|
5403
|
+
tier_bound: plan.bound,
|
|
5404
|
+
ledger_slot: plan.slot,
|
|
5405
|
+
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
5406
|
+
// against the pool without reading the ledger directory by hand.
|
|
5407
|
+
ledger_cap_usd: plan.capUsd,
|
|
5408
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
5409
|
+
}
|
|
4976
5410
|
});
|
|
4977
5411
|
}
|
|
4978
5412
|
|
|
@@ -5058,9 +5492,9 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5058
5492
|
}
|
|
5059
5493
|
|
|
5060
5494
|
// src/tools/memory/sync-config.ts
|
|
5061
|
-
import { homedir as
|
|
5062
|
-
import { join as
|
|
5063
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
5495
|
+
import { homedir as homedir6 } from "node:os";
|
|
5496
|
+
import { join as join8 } from "node:path";
|
|
5497
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4, readdirSync as readdirSync5 } from "node:fs";
|
|
5064
5498
|
init_safe_memory_file();
|
|
5065
5499
|
var TOOL_NAME22 = "vo_sync_config";
|
|
5066
5500
|
var inputSchema22 = {
|
|
@@ -5092,7 +5526,7 @@ function deriveProjectSlug(cwd) {
|
|
|
5092
5526
|
}
|
|
5093
5527
|
function getMemoryDir(cwd) {
|
|
5094
5528
|
const slug = deriveProjectSlug(cwd);
|
|
5095
|
-
return
|
|
5529
|
+
return join8(homedir6(), ".claude", "projects", slug, "memory");
|
|
5096
5530
|
}
|
|
5097
5531
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
5098
5532
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
@@ -5114,10 +5548,10 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
5114
5548
|
entry,
|
|
5115
5549
|
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
5116
5550
|
}));
|
|
5117
|
-
|
|
5551
|
+
mkdirSync5(memoryDir, { recursive: true });
|
|
5118
5552
|
const files = [];
|
|
5119
5553
|
for (const { entry, filePath } of writes) {
|
|
5120
|
-
|
|
5554
|
+
writeFileSync4(filePath, entry.content, "utf8");
|
|
5121
5555
|
files.push(entry.file_name);
|
|
5122
5556
|
}
|
|
5123
5557
|
return { pulled: data.entries.length, files };
|
|
@@ -5128,7 +5562,7 @@ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn)
|
|
|
5128
5562
|
}
|
|
5129
5563
|
const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
5130
5564
|
file_name: f,
|
|
5131
|
-
content:
|
|
5565
|
+
content: readFileSync9(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
5132
5566
|
entry_type: f === "MEMORY.md" ? "index" : "topic"
|
|
5133
5567
|
}));
|
|
5134
5568
|
if (localFiles.length === 0) {
|
|
@@ -5547,11 +5981,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
5547
5981
|
|
|
5548
5982
|
// src/tools/skills/skill-corpus.ts
|
|
5549
5983
|
import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
5550
|
-
import { dirname as dirname5, isAbsolute, join as
|
|
5984
|
+
import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve2 } from "node:path";
|
|
5551
5985
|
|
|
5552
5986
|
// ../skill-registry/src/loader.ts
|
|
5553
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
5554
|
-
import { join as
|
|
5987
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
|
|
5988
|
+
import { join as join9 } from "node:path";
|
|
5555
5989
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
5556
5990
|
constructor(skillFile, reason) {
|
|
5557
5991
|
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
@@ -5604,7 +6038,7 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
5604
6038
|
const entries = readdirSync6(skillsDir);
|
|
5605
6039
|
const skills = [];
|
|
5606
6040
|
for (const entry of entries) {
|
|
5607
|
-
const entryPath =
|
|
6041
|
+
const entryPath = join9(skillsDir, entry);
|
|
5608
6042
|
let stat;
|
|
5609
6043
|
try {
|
|
5610
6044
|
stat = statSync4(entryPath);
|
|
@@ -5612,10 +6046,10 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
5612
6046
|
continue;
|
|
5613
6047
|
}
|
|
5614
6048
|
if (!stat.isDirectory()) continue;
|
|
5615
|
-
const skillFile =
|
|
6049
|
+
const skillFile = join9(entryPath, "SKILL.md");
|
|
5616
6050
|
let raw;
|
|
5617
6051
|
try {
|
|
5618
|
-
raw =
|
|
6052
|
+
raw = readFileSync10(skillFile, "utf8");
|
|
5619
6053
|
} catch {
|
|
5620
6054
|
continue;
|
|
5621
6055
|
}
|
|
@@ -5660,7 +6094,7 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
5660
6094
|
}
|
|
5661
6095
|
let dir = resolve2(startDir);
|
|
5662
6096
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
5663
|
-
const candidate =
|
|
6097
|
+
const candidate = join10(dir, ".claude", "skills");
|
|
5664
6098
|
if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
|
|
5665
6099
|
const parent = dirname5(dir);
|
|
5666
6100
|
if (parent === dir) break;
|
|
@@ -6025,7 +6459,7 @@ function listToolNames() {
|
|
|
6025
6459
|
|
|
6026
6460
|
// src/cache/sqlite-cache.ts
|
|
6027
6461
|
import { createHash as createHash3 } from "node:crypto";
|
|
6028
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
6462
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6 } from "node:fs";
|
|
6029
6463
|
import { dirname as dirname6 } from "node:path";
|
|
6030
6464
|
import { DatabaseSync } from "node:sqlite";
|
|
6031
6465
|
|
|
@@ -6071,7 +6505,7 @@ function normalizeString(s) {
|
|
|
6071
6505
|
function createSqliteCache(options) {
|
|
6072
6506
|
const fileBacked = options.dbPath !== ":memory:";
|
|
6073
6507
|
if (fileBacked) {
|
|
6074
|
-
|
|
6508
|
+
mkdirSync6(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
6075
6509
|
}
|
|
6076
6510
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
6077
6511
|
const db = new DatabaseSync(options.dbPath);
|