@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/cli.js
CHANGED
|
@@ -1405,7 +1405,7 @@ __export(memory_knowledge_bridge_exports, {
|
|
|
1405
1405
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
1406
1406
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
1407
1407
|
});
|
|
1408
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
1408
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
1409
1409
|
function extractMemoryTitle(fileName, content) {
|
|
1410
1410
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1411
1411
|
if (frontmatter) {
|
|
@@ -1438,7 +1438,7 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
1438
1438
|
const failures = [];
|
|
1439
1439
|
for (const fileName of files) {
|
|
1440
1440
|
try {
|
|
1441
|
-
const content =
|
|
1441
|
+
const content = readFileSync8(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
1442
1442
|
if (content.length > CONTENT_HARD_LIMIT) {
|
|
1443
1443
|
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
1444
1444
|
continue;
|
|
@@ -1503,9 +1503,9 @@ __export(sync_config_exports, {
|
|
|
1503
1503
|
isNoopSyncReason: () => isNoopSyncReason,
|
|
1504
1504
|
runMemorySync: () => runMemorySync
|
|
1505
1505
|
});
|
|
1506
|
-
import { homedir as
|
|
1507
|
-
import { join as
|
|
1508
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
1506
|
+
import { homedir as homedir6 } from "node:os";
|
|
1507
|
+
import { join as join8 } from "node:path";
|
|
1508
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4, readdirSync as readdirSync5 } from "node:fs";
|
|
1509
1509
|
function isToolInput22(v) {
|
|
1510
1510
|
if (typeof v !== "object" || v === null) return false;
|
|
1511
1511
|
const o = v;
|
|
@@ -1518,7 +1518,7 @@ function deriveProjectSlug(cwd) {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
function getMemoryDir(cwd) {
|
|
1520
1520
|
const slug = deriveProjectSlug(cwd);
|
|
1521
|
-
return
|
|
1521
|
+
return join8(homedir6(), ".claude", "projects", slug, "memory");
|
|
1522
1522
|
}
|
|
1523
1523
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1524
1524
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
@@ -1540,10 +1540,10 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
1540
1540
|
entry,
|
|
1541
1541
|
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
1542
1542
|
}));
|
|
1543
|
-
|
|
1543
|
+
mkdirSync5(memoryDir, { recursive: true });
|
|
1544
1544
|
const files = [];
|
|
1545
1545
|
for (const { entry, filePath } of writes) {
|
|
1546
|
-
|
|
1546
|
+
writeFileSync4(filePath, entry.content, "utf8");
|
|
1547
1547
|
files.push(entry.file_name);
|
|
1548
1548
|
}
|
|
1549
1549
|
return { pulled: data.entries.length, files };
|
|
@@ -1554,7 +1554,7 @@ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn)
|
|
|
1554
1554
|
}
|
|
1555
1555
|
const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
1556
1556
|
file_name: f,
|
|
1557
|
-
content:
|
|
1557
|
+
content: readFileSync9(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
1558
1558
|
entry_type: f === "MEMORY.md" ? "index" : "topic"
|
|
1559
1559
|
}));
|
|
1560
1560
|
if (localFiles.length === 0) {
|
|
@@ -1734,9 +1734,9 @@ var init_sync_config = __esm({
|
|
|
1734
1734
|
});
|
|
1735
1735
|
|
|
1736
1736
|
// src/cli.ts
|
|
1737
|
-
import { homedir as
|
|
1737
|
+
import { homedir as homedir7, hostname } from "node:os";
|
|
1738
1738
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
1739
|
-
import { join as
|
|
1739
|
+
import { join as join11 } from "node:path";
|
|
1740
1740
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1741
1741
|
|
|
1742
1742
|
// src/server.ts
|
|
@@ -5394,9 +5394,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5394
5394
|
// src/tools/session/spawn-successor.ts
|
|
5395
5395
|
init_common();
|
|
5396
5396
|
import { spawn } from "node:child_process";
|
|
5397
|
+
import { homedir as homedir5 } from "node:os";
|
|
5398
|
+
import { join as join7 } from "node:path";
|
|
5399
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
5400
|
+
|
|
5401
|
+
// src/swarm/tier-binding.ts
|
|
5402
|
+
var SWARM_TIERS = Object.freeze([
|
|
5403
|
+
"tier1_subscription",
|
|
5404
|
+
"tier1_local",
|
|
5405
|
+
"tier2_user_key",
|
|
5406
|
+
"tier3_platform_key",
|
|
5407
|
+
"refused",
|
|
5408
|
+
"unresolved"
|
|
5409
|
+
]);
|
|
5410
|
+
var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
|
|
5411
|
+
"tier1_subscription",
|
|
5412
|
+
"tier1_local",
|
|
5413
|
+
"tier2_user_key",
|
|
5414
|
+
"tier3_platform_key"
|
|
5415
|
+
]);
|
|
5416
|
+
var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
|
|
5417
|
+
var MAX_BOUND_SUBAGENTS = 20;
|
|
5418
|
+
function isPositiveCap(cap) {
|
|
5419
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
|
|
5420
|
+
}
|
|
5421
|
+
function unresolvedBinding(swarmId, nowIso, reason) {
|
|
5422
|
+
return {
|
|
5423
|
+
schema_version: 1,
|
|
5424
|
+
swarm_id: swarmId,
|
|
5425
|
+
tier: "unresolved",
|
|
5426
|
+
agent: null,
|
|
5427
|
+
reason,
|
|
5428
|
+
exhausted_agents: [],
|
|
5429
|
+
subagent_budget: 0,
|
|
5430
|
+
spend_cap_usd: null,
|
|
5431
|
+
resolved_at: nowIso
|
|
5432
|
+
};
|
|
5433
|
+
}
|
|
5434
|
+
function serializeSwarmTierBinding(binding) {
|
|
5435
|
+
return JSON.stringify(binding);
|
|
5436
|
+
}
|
|
5437
|
+
function parseSwarmTierBinding(raw, nowIso) {
|
|
5438
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
5439
|
+
return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
|
|
5440
|
+
}
|
|
5441
|
+
let parsed;
|
|
5442
|
+
try {
|
|
5443
|
+
parsed = JSON.parse(raw);
|
|
5444
|
+
} catch {
|
|
5445
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
|
|
5446
|
+
}
|
|
5447
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5448
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
|
|
5449
|
+
}
|
|
5450
|
+
const o = parsed;
|
|
5451
|
+
const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
|
|
5452
|
+
if (o["schema_version"] !== 1) {
|
|
5453
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
|
|
5454
|
+
}
|
|
5455
|
+
const tier = o["tier"];
|
|
5456
|
+
if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
|
|
5457
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
|
|
5458
|
+
}
|
|
5459
|
+
const budget = o["subagent_budget"];
|
|
5460
|
+
const cap = o["spend_cap_usd"];
|
|
5461
|
+
const capNum = isPositiveCap(cap) ? cap : null;
|
|
5462
|
+
if (tier === "tier3_platform_key" && capNum === null) {
|
|
5463
|
+
return unresolvedBinding(
|
|
5464
|
+
swarmId,
|
|
5465
|
+
nowIso,
|
|
5466
|
+
"inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
|
|
5467
|
+
);
|
|
5468
|
+
}
|
|
5469
|
+
return {
|
|
5470
|
+
schema_version: 1,
|
|
5471
|
+
swarm_id: swarmId,
|
|
5472
|
+
tier,
|
|
5473
|
+
agent: typeof o["agent"] === "string" ? o["agent"] : null,
|
|
5474
|
+
reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
|
|
5475
|
+
exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
|
|
5476
|
+
subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
|
|
5477
|
+
spend_cap_usd: capNum,
|
|
5478
|
+
resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
|
|
5479
|
+
};
|
|
5480
|
+
}
|
|
5481
|
+
function inheritSwarmTierBinding(env, nowIso) {
|
|
5482
|
+
return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
|
|
5483
|
+
}
|
|
5484
|
+
function bindingEnvFragment(binding) {
|
|
5485
|
+
return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
|
|
5486
|
+
}
|
|
5487
|
+
function childBindingEnvFragment(binding, allocatedCapUsd = null) {
|
|
5488
|
+
return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
|
|
5489
|
+
}
|
|
5490
|
+
function admitSubagentSpawn(binding, spawnsSoFar = 0) {
|
|
5491
|
+
if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
|
|
5492
|
+
return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
|
|
5493
|
+
}
|
|
5494
|
+
if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
|
|
5495
|
+
return {
|
|
5496
|
+
allowed: false,
|
|
5497
|
+
reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5500
|
+
if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
|
|
5501
|
+
return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
|
|
5502
|
+
}
|
|
5503
|
+
if (spawnsSoFar >= binding.subagent_budget) {
|
|
5504
|
+
return {
|
|
5505
|
+
allowed: false,
|
|
5506
|
+
reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
|
|
5510
|
+
}
|
|
5511
|
+
function childBinding(binding, allocatedCapUsd = null) {
|
|
5512
|
+
const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
|
|
5513
|
+
const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
|
|
5514
|
+
return {
|
|
5515
|
+
...binding,
|
|
5516
|
+
subagent_budget: Math.max(0, binding.subagent_budget - 1),
|
|
5517
|
+
// A child never carries more than its parent, whatever the ledger says: a
|
|
5518
|
+
// forged or hand-edited pool cannot inflate a descendant above the binding
|
|
5519
|
+
// it descends from.
|
|
5520
|
+
spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
|
|
5521
|
+
};
|
|
5522
|
+
}
|
|
5523
|
+
function agentBindingRefusal(binding, requestedAgent) {
|
|
5524
|
+
const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
|
|
5525
|
+
if (requested.length === 0) return null;
|
|
5526
|
+
if (binding.agent !== null && requested === binding.agent) return null;
|
|
5527
|
+
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)`;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
// src/swarm/successor-launch.ts
|
|
5531
|
+
var AGENT_LAUNCH_SHAPES = Object.freeze({
|
|
5532
|
+
claude: {
|
|
5533
|
+
bin: "claude",
|
|
5534
|
+
baseArgs: ["-p", "--permission-mode", "acceptEdits"],
|
|
5535
|
+
enforcesMaxTurns: true,
|
|
5536
|
+
maxTurnsFlag: "--max-turns",
|
|
5537
|
+
windowsShellSafe: true
|
|
5538
|
+
},
|
|
5539
|
+
codex: {
|
|
5540
|
+
bin: "codex",
|
|
5541
|
+
baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
5542
|
+
enforcesMaxTurns: false,
|
|
5543
|
+
// `-` makes codex read the prompt from stdin (injection-safe), matching how
|
|
5544
|
+
// codex-runner.mjs already spawns it.
|
|
5545
|
+
trailingArgs: ["-"],
|
|
5546
|
+
// `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
|
|
5547
|
+
// unverified, so win32 refuses rather than risking a mangled sandbox flag.
|
|
5548
|
+
windowsShellSafe: false
|
|
5549
|
+
}
|
|
5550
|
+
});
|
|
5551
|
+
function resolveSuccessorLaunch(input) {
|
|
5552
|
+
const agent = typeof input.agent === "string" ? input.agent.trim() : "";
|
|
5553
|
+
if (!agent) {
|
|
5554
|
+
return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
|
|
5555
|
+
}
|
|
5556
|
+
const shape = AGENT_LAUNCH_SHAPES[agent];
|
|
5557
|
+
if (!shape) {
|
|
5558
|
+
const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
|
|
5559
|
+
return {
|
|
5560
|
+
ok: false,
|
|
5561
|
+
reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5564
|
+
const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
|
|
5565
|
+
if (wantsMaxTurns && !shape.enforcesMaxTurns) {
|
|
5566
|
+
return {
|
|
5567
|
+
ok: false,
|
|
5568
|
+
reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
|
|
5569
|
+
};
|
|
5570
|
+
}
|
|
5571
|
+
const platform = input.platform ?? process.platform;
|
|
5572
|
+
if (platform === "win32" && !shape.windowsShellSafe) {
|
|
5573
|
+
return {
|
|
5574
|
+
ok: false,
|
|
5575
|
+
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`
|
|
5576
|
+
};
|
|
5577
|
+
}
|
|
5578
|
+
const args = [...shape.baseArgs];
|
|
5579
|
+
if (wantsMaxTurns && shape.maxTurnsFlag) {
|
|
5580
|
+
args.push(shape.maxTurnsFlag, String(input.maxTurns));
|
|
5581
|
+
}
|
|
5582
|
+
if (shape.trailingArgs) args.push(...shape.trailingArgs);
|
|
5583
|
+
return { ok: true, agent, bin: shape.bin, args };
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
// src/swarm/spawn-ledger.ts
|
|
5587
|
+
import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5397
5588
|
import { homedir as homedir4 } from "node:os";
|
|
5398
5589
|
import { join as join6 } from "node:path";
|
|
5399
|
-
|
|
5590
|
+
var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
|
|
5591
|
+
function resolveLedgerDir(env) {
|
|
5592
|
+
const override = env[SWARM_LEDGER_DIR_ENV];
|
|
5593
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
5594
|
+
return join6(homedir4(), ".vo", "swarm-ledger");
|
|
5595
|
+
}
|
|
5596
|
+
function sanitizeSwarmId(raw) {
|
|
5597
|
+
if (typeof raw !== "string") return null;
|
|
5598
|
+
const id = raw.trim();
|
|
5599
|
+
if (id.length === 0 || id.length > 128) return null;
|
|
5600
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
|
|
5601
|
+
if (id === "." || id === "..") return null;
|
|
5602
|
+
return id;
|
|
5603
|
+
}
|
|
5604
|
+
var CEILING_FILE = "ceiling.json";
|
|
5605
|
+
function createExclusive(path3, contents) {
|
|
5606
|
+
let fd;
|
|
5607
|
+
try {
|
|
5608
|
+
fd = openSync(path3, "wx");
|
|
5609
|
+
} catch {
|
|
5610
|
+
return false;
|
|
5611
|
+
}
|
|
5612
|
+
try {
|
|
5613
|
+
writeFileSync3(fd, contents, "utf8");
|
|
5614
|
+
} finally {
|
|
5615
|
+
closeSync(fd);
|
|
5616
|
+
}
|
|
5617
|
+
return true;
|
|
5618
|
+
}
|
|
5619
|
+
function capToCents(cap) {
|
|
5620
|
+
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
5621
|
+
}
|
|
5622
|
+
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
5623
|
+
const path3 = join6(swarmDir, CEILING_FILE);
|
|
5624
|
+
const head = JSON.stringify({
|
|
5625
|
+
ceiling: proposedCeiling,
|
|
5626
|
+
cap_cents: proposedCapCents,
|
|
5627
|
+
recorded_at: nowIso
|
|
5628
|
+
});
|
|
5629
|
+
if (createExclusive(path3, head)) {
|
|
5630
|
+
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
5631
|
+
}
|
|
5632
|
+
let parsed;
|
|
5633
|
+
try {
|
|
5634
|
+
parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
5635
|
+
} catch {
|
|
5636
|
+
return null;
|
|
5637
|
+
}
|
|
5638
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5639
|
+
const record = parsed;
|
|
5640
|
+
const recorded = record["ceiling"];
|
|
5641
|
+
if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
|
|
5642
|
+
const recordedCap = record["cap_cents"];
|
|
5643
|
+
const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
|
|
5644
|
+
return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
|
|
5645
|
+
}
|
|
5646
|
+
var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
|
|
5647
|
+
const id = sanitizeSwarmId(swarmId);
|
|
5648
|
+
if (id === null) {
|
|
5649
|
+
return {
|
|
5650
|
+
ok: false,
|
|
5651
|
+
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`
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
|
|
5655
|
+
if (proposed < 1) {
|
|
5656
|
+
return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
|
|
5657
|
+
}
|
|
5658
|
+
const swarmDir = join6(dir, id);
|
|
5659
|
+
try {
|
|
5660
|
+
mkdirSync3(swarmDir, { recursive: true });
|
|
5661
|
+
} catch (err) {
|
|
5662
|
+
return {
|
|
5663
|
+
ok: false,
|
|
5664
|
+
reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
|
|
5665
|
+
};
|
|
5666
|
+
}
|
|
5667
|
+
const wantedCents = capToCents(proposedCapUsd);
|
|
5668
|
+
const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
|
|
5669
|
+
if (head === null) {
|
|
5670
|
+
return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
|
|
5671
|
+
}
|
|
5672
|
+
const { ceiling, capCents } = head;
|
|
5673
|
+
const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
|
|
5674
|
+
if (wantedCents > 0 && shareCents < 1) {
|
|
5675
|
+
return {
|
|
5676
|
+
ok: false,
|
|
5677
|
+
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`
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
for (let slot = 0; slot < ceiling; slot++) {
|
|
5681
|
+
const debitedCents = shareCents;
|
|
5682
|
+
const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
|
|
5683
|
+
const claimed = createExclusive(
|
|
5684
|
+
join6(swarmDir, `slot-${slot}.json`),
|
|
5685
|
+
JSON.stringify({
|
|
5686
|
+
slot,
|
|
5687
|
+
ceiling,
|
|
5688
|
+
pid: process.pid,
|
|
5689
|
+
claimed_at: nowIso,
|
|
5690
|
+
// The debit record. Durable and atomic with the claim: this file is
|
|
5691
|
+
// created with O_EXCL, so exactly one claimant ever writes this line.
|
|
5692
|
+
cap_cents_pool: capCents,
|
|
5693
|
+
cap_cents_debited: debitedCents,
|
|
5694
|
+
cap_cents_remaining: remainingCents
|
|
5695
|
+
})
|
|
5696
|
+
);
|
|
5697
|
+
if (claimed) {
|
|
5698
|
+
return {
|
|
5699
|
+
ok: true,
|
|
5700
|
+
slot,
|
|
5701
|
+
ceiling,
|
|
5702
|
+
remaining: ceiling - slot - 1,
|
|
5703
|
+
capUsd: debitedCents > 0 ? debitedCents / 100 : null,
|
|
5704
|
+
capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
return {
|
|
5709
|
+
ok: false,
|
|
5710
|
+
reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
|
|
5711
|
+
};
|
|
5712
|
+
};
|
|
5713
|
+
|
|
5714
|
+
// src/tools/session/spawn-successor.ts
|
|
5400
5715
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
5401
5716
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
5402
5717
|
var inputSchema20 = {
|
|
@@ -5417,11 +5732,16 @@ var inputSchema20 = {
|
|
|
5417
5732
|
max_turns: {
|
|
5418
5733
|
type: "number",
|
|
5419
5734
|
description: "Optional --max-turns bound for the successor."
|
|
5735
|
+
},
|
|
5736
|
+
agent: {
|
|
5737
|
+
type: "string",
|
|
5738
|
+
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.`
|
|
5420
5739
|
}
|
|
5421
5740
|
},
|
|
5422
5741
|
required: [],
|
|
5423
5742
|
additionalProperties: false
|
|
5424
5743
|
};
|
|
5744
|
+
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
5425
5745
|
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.";
|
|
5426
5746
|
function isToolInput20(v) {
|
|
5427
5747
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -5430,12 +5750,18 @@ function isToolInput20(v) {
|
|
|
5430
5750
|
if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
|
|
5431
5751
|
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
5432
5752
|
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
5753
|
+
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
5433
5754
|
return true;
|
|
5434
5755
|
}
|
|
5435
|
-
function
|
|
5756
|
+
function retiredCounterRefusal(v) {
|
|
5757
|
+
if (typeof v !== "object" || v === null) return null;
|
|
5758
|
+
if (!(RETIRED_COUNTER_INPUT in v)) return null;
|
|
5759
|
+
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.`;
|
|
5760
|
+
}
|
|
5761
|
+
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
5436
5762
|
try {
|
|
5437
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(
|
|
5438
|
-
return entries.length > 0 && entries[0] ?
|
|
5763
|
+
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);
|
|
5764
|
+
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
5439
5765
|
} catch {
|
|
5440
5766
|
return null;
|
|
5441
5767
|
}
|
|
@@ -5479,9 +5805,87 @@ function buildSuccessorArgs(maxTurns) {
|
|
|
5479
5805
|
}
|
|
5480
5806
|
return args;
|
|
5481
5807
|
}
|
|
5808
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
5809
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
5810
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
5811
|
+
if (!hasBinding) {
|
|
5812
|
+
const explicit = input.agent?.trim();
|
|
5813
|
+
if (explicit) {
|
|
5814
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
5815
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
5816
|
+
return {
|
|
5817
|
+
ok: true,
|
|
5818
|
+
bin: resolved2.bin,
|
|
5819
|
+
args: resolved2.args,
|
|
5820
|
+
agent: resolved2.agent,
|
|
5821
|
+
tier: "unbound",
|
|
5822
|
+
bound: false,
|
|
5823
|
+
env: {},
|
|
5824
|
+
slot: null,
|
|
5825
|
+
capUsd: null,
|
|
5826
|
+
capRemainingUsd: null
|
|
5827
|
+
};
|
|
5828
|
+
}
|
|
5829
|
+
return {
|
|
5830
|
+
ok: true,
|
|
5831
|
+
bin: "claude",
|
|
5832
|
+
args: buildSuccessorArgs(input.max_turns),
|
|
5833
|
+
agent: "claude",
|
|
5834
|
+
tier: "unbound",
|
|
5835
|
+
bound: false,
|
|
5836
|
+
env: {},
|
|
5837
|
+
slot: null,
|
|
5838
|
+
capUsd: null,
|
|
5839
|
+
capRemainingUsd: null
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
5843
|
+
const admission = admitSubagentSpawn(binding);
|
|
5844
|
+
if (!admission.allowed) {
|
|
5845
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
5846
|
+
}
|
|
5847
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
5848
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
5849
|
+
const resolved = resolveSuccessorLaunch({
|
|
5850
|
+
agent: binding.agent,
|
|
5851
|
+
maxTurns: input.max_turns,
|
|
5852
|
+
platform
|
|
5853
|
+
});
|
|
5854
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
5855
|
+
const slot = claim({
|
|
5856
|
+
swarmId: binding.swarm_id,
|
|
5857
|
+
proposedCeiling: binding.subagent_budget,
|
|
5858
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
5859
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
5860
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
5861
|
+
dir: resolveLedgerDir(env),
|
|
5862
|
+
nowIso
|
|
5863
|
+
});
|
|
5864
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
5865
|
+
return {
|
|
5866
|
+
ok: true,
|
|
5867
|
+
bin: resolved.bin,
|
|
5868
|
+
args: resolved.args,
|
|
5869
|
+
agent: resolved.agent,
|
|
5870
|
+
tier: binding.tier,
|
|
5871
|
+
bound: true,
|
|
5872
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
5873
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
5874
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
5875
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
5876
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
5877
|
+
// untouched $50 and walked away with $75 between them.
|
|
5878
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
5879
|
+
slot: slot.slot,
|
|
5880
|
+
capUsd: slot.capUsd,
|
|
5881
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
5882
|
+
};
|
|
5883
|
+
}
|
|
5482
5884
|
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
5885
|
+
const retired = retiredCounterRefusal(rawInput);
|
|
5886
|
+
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
5483
5887
|
if (!isToolInput20(rawInput)) {
|
|
5484
|
-
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
|
|
5888
|
+
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
5485
5889
|
}
|
|
5486
5890
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
5487
5891
|
if (!handoffPath || !existsSync4(handoffPath)) {
|
|
@@ -5494,20 +5898,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5494
5898
|
}
|
|
5495
5899
|
});
|
|
5496
5900
|
}
|
|
5497
|
-
const handoff =
|
|
5901
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
5498
5902
|
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5903
|
+
const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
|
|
5904
|
+
if (!plan.ok) {
|
|
5905
|
+
return jsonContent({
|
|
5906
|
+
tool: TOOL_NAME20,
|
|
5907
|
+
schema_version: 1,
|
|
5908
|
+
payload: {
|
|
5909
|
+
spawned: false,
|
|
5910
|
+
reason: `swarm tier binding refused this spawn: ${plan.reason}`,
|
|
5911
|
+
tier: plan.tier,
|
|
5912
|
+
handoff_path: handoffPath
|
|
5913
|
+
}
|
|
5914
|
+
});
|
|
5915
|
+
}
|
|
5916
|
+
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
5917
|
+
mkdirSync4(logDir, { recursive: true });
|
|
5918
|
+
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
5919
|
+
const logFd = openSync2(logPath, "a");
|
|
5920
|
+
const child = spawnImpl(plan.bin, [...plan.args], {
|
|
5504
5921
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
5505
5922
|
detached: true,
|
|
5506
5923
|
stdio: ["pipe", logFd, logFd],
|
|
5507
|
-
// Windows:
|
|
5508
|
-
// goes via STDIN below, never argv, so the shell never sees it.
|
|
5924
|
+
// Windows: the agent CLIs are .cmd shims — they need a shell to resolve.
|
|
5925
|
+
// The prompt goes via STDIN below, never argv, so the shell never sees it.
|
|
5509
5926
|
shell: process.platform === "win32",
|
|
5510
|
-
windowsHide: true
|
|
5927
|
+
windowsHide: true,
|
|
5928
|
+
// Carry the SAME binding to the child. Without this the successor inherits
|
|
5929
|
+
// no tier and re-resolves its own — which is the split-payer defect one
|
|
5930
|
+
// generation down.
|
|
5931
|
+
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
5511
5932
|
});
|
|
5512
5933
|
let spawnError = null;
|
|
5513
5934
|
child.on("error", (e) => {
|
|
@@ -5523,7 +5944,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5523
5944
|
return jsonContent({
|
|
5524
5945
|
tool: TOOL_NAME20,
|
|
5525
5946
|
schema_version: 1,
|
|
5526
|
-
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`,
|
|
5947
|
+
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
|
|
5948
|
+
spawned: true,
|
|
5949
|
+
pid: child.pid ?? null,
|
|
5950
|
+
log_path: logPath,
|
|
5951
|
+
handoff_path: handoffPath,
|
|
5952
|
+
agent: plan.agent,
|
|
5953
|
+
tier: plan.tier,
|
|
5954
|
+
tier_bound: plan.bound,
|
|
5955
|
+
ledger_slot: plan.slot,
|
|
5956
|
+
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
5957
|
+
// against the pool without reading the ledger directory by hand.
|
|
5958
|
+
ledger_cap_usd: plan.capUsd,
|
|
5959
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
5960
|
+
}
|
|
5527
5961
|
});
|
|
5528
5962
|
}
|
|
5529
5963
|
|
|
@@ -5886,11 +6320,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
5886
6320
|
|
|
5887
6321
|
// src/tools/skills/skill-corpus.ts
|
|
5888
6322
|
import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
5889
|
-
import { dirname as dirname5, isAbsolute, join as
|
|
6323
|
+
import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve2 } from "node:path";
|
|
5890
6324
|
|
|
5891
6325
|
// ../skill-registry/src/loader.ts
|
|
5892
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
5893
|
-
import { join as
|
|
6326
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
|
|
6327
|
+
import { join as join9 } from "node:path";
|
|
5894
6328
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
5895
6329
|
constructor(skillFile, reason) {
|
|
5896
6330
|
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
@@ -5943,7 +6377,7 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
5943
6377
|
const entries = readdirSync6(skillsDir);
|
|
5944
6378
|
const skills = [];
|
|
5945
6379
|
for (const entry of entries) {
|
|
5946
|
-
const entryPath =
|
|
6380
|
+
const entryPath = join9(skillsDir, entry);
|
|
5947
6381
|
let stat;
|
|
5948
6382
|
try {
|
|
5949
6383
|
stat = statSync4(entryPath);
|
|
@@ -5951,10 +6385,10 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
5951
6385
|
continue;
|
|
5952
6386
|
}
|
|
5953
6387
|
if (!stat.isDirectory()) continue;
|
|
5954
|
-
const skillFile =
|
|
6388
|
+
const skillFile = join9(entryPath, "SKILL.md");
|
|
5955
6389
|
let raw;
|
|
5956
6390
|
try {
|
|
5957
|
-
raw =
|
|
6391
|
+
raw = readFileSync10(skillFile, "utf8");
|
|
5958
6392
|
} catch {
|
|
5959
6393
|
continue;
|
|
5960
6394
|
}
|
|
@@ -6000,7 +6434,7 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
6000
6434
|
}
|
|
6001
6435
|
let dir = resolve2(startDir);
|
|
6002
6436
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6003
|
-
const candidate =
|
|
6437
|
+
const candidate = join10(dir, ".claude", "skills");
|
|
6004
6438
|
if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
|
|
6005
6439
|
const parent = dirname5(dir);
|
|
6006
6440
|
if (parent === dir) break;
|
|
@@ -6362,7 +6796,7 @@ function createServer(options) {
|
|
|
6362
6796
|
|
|
6363
6797
|
// src/cache/sqlite-cache.ts
|
|
6364
6798
|
import { createHash as createHash3 } from "node:crypto";
|
|
6365
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
6799
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync6 } from "node:fs";
|
|
6366
6800
|
import { dirname as dirname6 } from "node:path";
|
|
6367
6801
|
import { DatabaseSync } from "node:sqlite";
|
|
6368
6802
|
|
|
@@ -6408,7 +6842,7 @@ function normalizeString(s) {
|
|
|
6408
6842
|
function createSqliteCache(options) {
|
|
6409
6843
|
const fileBacked = options.dbPath !== ":memory:";
|
|
6410
6844
|
if (fileBacked) {
|
|
6411
|
-
|
|
6845
|
+
mkdirSync6(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
6412
6846
|
}
|
|
6413
6847
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
6414
6848
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -7410,7 +7844,7 @@ init_common();
|
|
|
7410
7844
|
function defaultCacheDbPath() {
|
|
7411
7845
|
const env = process.env["VO_MCP_DB_PATH"];
|
|
7412
7846
|
if (env && env.length > 0) return env;
|
|
7413
|
-
return
|
|
7847
|
+
return join11(homedir7(), ".claude", "vo-mcp-cache.db");
|
|
7414
7848
|
}
|
|
7415
7849
|
async function probeEngineVersion() {
|
|
7416
7850
|
try {
|
|
@@ -7561,12 +7995,12 @@ if (process.argv[2] === "login") {
|
|
|
7561
7995
|
const sessionId = randomUUID5();
|
|
7562
7996
|
const appendSyncLog = async (line) => {
|
|
7563
7997
|
try {
|
|
7564
|
-
const { appendFileSync: appendFileSync2, mkdirSync:
|
|
7565
|
-
const { join:
|
|
7566
|
-
const { homedir:
|
|
7567
|
-
const dir =
|
|
7568
|
-
|
|
7569
|
-
appendFileSync2(
|
|
7998
|
+
const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync7 } = await import("node:fs");
|
|
7999
|
+
const { join: join12 } = await import("node:path");
|
|
8000
|
+
const { homedir: homedir8 } = await import("node:os");
|
|
8001
|
+
const dir = join12(homedir8(), ".claude");
|
|
8002
|
+
mkdirSync7(dir, { recursive: true });
|
|
8003
|
+
appendFileSync2(join12(dir, "vo-mcp-sync.log"), `${line}
|
|
7570
8004
|
`, "utf8");
|
|
7571
8005
|
} catch {
|
|
7572
8006
|
}
|