multicorn-shield 1.11.0 → 1.12.0
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/CHANGELOG.md +31 -0
- package/README.md +10 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/multicorn-proxy.js +349 -212
- package/dist/multicorn-shield.js +340 -209
- package/dist/openclaw-hook/HOOK.md +5 -3
- package/dist/openclaw-hook/handler.js +1 -1
- package/dist/openclaw-plugin/multicorn-shield.js +28 -7
- package/dist/openclaw-plugin/openclaw.plugin.json +1 -1
- package/dist/server.js +3499 -0
- package/dist/shield-extension.js +3 -12
- package/package.json +5 -3
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +126 -17
package/dist/multicorn-proxy.js
CHANGED
|
@@ -644,7 +644,34 @@ function isVersionAtLeast(version, minimum) {
|
|
|
644
644
|
}
|
|
645
645
|
return true;
|
|
646
646
|
}
|
|
647
|
-
|
|
647
|
+
function buildOpenClawShieldPluginConfig(params) {
|
|
648
|
+
return {
|
|
649
|
+
apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
|
|
650
|
+
baseUrl: params.baseUrl,
|
|
651
|
+
agentName: params.agentName,
|
|
652
|
+
failMode: "closed"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function detectStaleOpenClawHookIntegration(openclawConfig) {
|
|
656
|
+
const hooks = openclawConfig["hooks"];
|
|
657
|
+
if (typeof hooks !== "object" || hooks === null) return false;
|
|
658
|
+
const internal = hooks["internal"];
|
|
659
|
+
if (typeof internal !== "object" || internal === null) return false;
|
|
660
|
+
const entries = internal["entries"];
|
|
661
|
+
if (typeof entries !== "object" || entries === null) return false;
|
|
662
|
+
return Object.prototype.hasOwnProperty.call(entries, "multicorn-shield");
|
|
663
|
+
}
|
|
664
|
+
function staleOpenClawHookIntegrationMessage() {
|
|
665
|
+
return "Deprecated OpenClaw gateway hook config detected at hooks.internal.entries.multicorn-shield. Shield now uses the multicorn-shield plugin under plugins.entries. Disable the old hook manually (for example openclaw hooks disable multicorn-shield) and remove hooks.internal.entries.multicorn-shield when you are ready.";
|
|
666
|
+
}
|
|
667
|
+
function stripLegacyOpenClawShieldEntryFields(shield) {
|
|
668
|
+
delete shield["env"];
|
|
669
|
+
delete shield["apiKey"];
|
|
670
|
+
delete shield["baseUrl"];
|
|
671
|
+
delete shield["agentName"];
|
|
672
|
+
delete shield["failMode"];
|
|
673
|
+
}
|
|
674
|
+
async function updateOpenClawConfigIfPresent(_apiKey, baseUrl, agentName) {
|
|
648
675
|
let raw;
|
|
649
676
|
try {
|
|
650
677
|
raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
@@ -660,50 +687,67 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
|
660
687
|
} catch {
|
|
661
688
|
return "parse-error";
|
|
662
689
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
690
|
+
if (detectStaleOpenClawHookIntegration(obj)) {
|
|
691
|
+
process.stderr.write(
|
|
692
|
+
style.yellow("\u26A0") + " " + staleOpenClawHookIntegrationMessage() + "\n"
|
|
693
|
+
);
|
|
667
694
|
}
|
|
668
|
-
let
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
695
|
+
let plugins = obj["plugins"];
|
|
696
|
+
if (plugins === void 0 || typeof plugins !== "object") {
|
|
697
|
+
plugins = {};
|
|
698
|
+
obj["plugins"] = plugins;
|
|
672
699
|
}
|
|
673
|
-
let
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
700
|
+
let pluginEntries = plugins["entries"];
|
|
701
|
+
if (pluginEntries === void 0 || typeof pluginEntries !== "object") {
|
|
702
|
+
pluginEntries = {};
|
|
703
|
+
plugins["entries"] = pluginEntries;
|
|
677
704
|
}
|
|
678
|
-
let shield =
|
|
705
|
+
let shield = pluginEntries["multicorn-shield"];
|
|
679
706
|
if (shield === void 0 || typeof shield !== "object") {
|
|
680
|
-
shield = { enabled: true
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
shield["
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
env["MULTICORN_AGENT_NAME"] = agentName;
|
|
707
|
+
shield = { enabled: true };
|
|
708
|
+
pluginEntries["multicorn-shield"] = shield;
|
|
709
|
+
}
|
|
710
|
+
stripLegacyOpenClawShieldEntryFields(shield);
|
|
711
|
+
const resolvedAgentName = agentName ?? (typeof shield["config"] === "object" && shield["config"] !== null && typeof shield["config"]["agentName"] === "string" ? String(shield["config"]["agentName"]) : void 0);
|
|
712
|
+
if (resolvedAgentName !== void 0) {
|
|
713
|
+
shield["enabled"] = true;
|
|
714
|
+
shield["config"] = buildOpenClawShieldPluginConfig({
|
|
715
|
+
baseUrl,
|
|
716
|
+
agentName: resolvedAgentName
|
|
717
|
+
});
|
|
692
718
|
const agentsList = obj["agents"];
|
|
693
719
|
const list = agentsList?.["list"];
|
|
694
720
|
if (Array.isArray(list) && list.length > 0) {
|
|
695
721
|
const first = list[0];
|
|
696
|
-
if (first["id"] !==
|
|
697
|
-
first["id"] =
|
|
698
|
-
first["name"] =
|
|
722
|
+
if (first["id"] !== resolvedAgentName) {
|
|
723
|
+
first["id"] = resolvedAgentName;
|
|
724
|
+
first["name"] = resolvedAgentName;
|
|
699
725
|
}
|
|
700
726
|
} else {
|
|
701
727
|
if (agentsList !== void 0 && typeof agentsList === "object") {
|
|
702
|
-
agentsList["list"] = [{ id:
|
|
728
|
+
agentsList["list"] = [{ id: resolvedAgentName, name: resolvedAgentName }];
|
|
703
729
|
} else {
|
|
704
|
-
obj["agents"] = { list: [{ id:
|
|
730
|
+
obj["agents"] = { list: [{ id: resolvedAgentName, name: resolvedAgentName }] };
|
|
705
731
|
}
|
|
706
732
|
}
|
|
733
|
+
} else {
|
|
734
|
+
shield["enabled"] = true;
|
|
735
|
+
const existingConfig = shield["config"];
|
|
736
|
+
if (typeof existingConfig === "object" && existingConfig !== null) {
|
|
737
|
+
const cfg = { ...existingConfig };
|
|
738
|
+
cfg["apiKey"] = OPENCLAW_SHIELD_API_KEY_ENV_REF;
|
|
739
|
+
cfg["baseUrl"] = baseUrl;
|
|
740
|
+
if (cfg["failMode"] !== "open" && cfg["failMode"] !== "closed") {
|
|
741
|
+
cfg["failMode"] = "closed";
|
|
742
|
+
}
|
|
743
|
+
shield["config"] = cfg;
|
|
744
|
+
} else {
|
|
745
|
+
shield["config"] = {
|
|
746
|
+
apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
|
|
747
|
+
baseUrl,
|
|
748
|
+
failMode: "closed"
|
|
749
|
+
};
|
|
750
|
+
}
|
|
707
751
|
}
|
|
708
752
|
await writeFile(
|
|
709
753
|
OPENCLAW_CONFIG_PATH,
|
|
@@ -712,6 +756,10 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
|
|
|
712
756
|
);
|
|
713
757
|
return "updated";
|
|
714
758
|
}
|
|
759
|
+
function isPastedShieldApiKeyInput(input) {
|
|
760
|
+
const trimmed = input.trim();
|
|
761
|
+
return trimmed.startsWith("mcs_") && trimmed.length >= 8;
|
|
762
|
+
}
|
|
715
763
|
async function validateApiKey(apiKey, baseUrl) {
|
|
716
764
|
try {
|
|
717
765
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
@@ -1081,40 +1129,6 @@ async function installCodexCliNativeHooks() {
|
|
|
1081
1129
|
};
|
|
1082
1130
|
await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
|
|
1083
1131
|
}
|
|
1084
|
-
async function promptCodexCliIntegrationMode(ask) {
|
|
1085
|
-
process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
|
|
1086
|
-
process.stderr.write(
|
|
1087
|
-
" " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
|
|
1088
|
-
);
|
|
1089
|
-
process.stderr.write(
|
|
1090
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
|
|
1091
|
-
);
|
|
1092
|
-
let choice = 0;
|
|
1093
|
-
while (choice === 0) {
|
|
1094
|
-
const input = await ask("Choose integration (1-2): ");
|
|
1095
|
-
const num = parseInt(input.trim(), 10);
|
|
1096
|
-
if (num === 1) choice = 1;
|
|
1097
|
-
if (num === 2) choice = 2;
|
|
1098
|
-
}
|
|
1099
|
-
return choice === 1 ? "native" : "hosted";
|
|
1100
|
-
}
|
|
1101
|
-
async function promptClineIntegrationMode(ask) {
|
|
1102
|
-
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
1103
|
-
process.stderr.write(
|
|
1104
|
-
" " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
|
|
1105
|
-
);
|
|
1106
|
-
process.stderr.write(
|
|
1107
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
|
|
1108
|
-
);
|
|
1109
|
-
let choice = 0;
|
|
1110
|
-
while (choice === 0) {
|
|
1111
|
-
const input = await ask("Choose integration (1-2): ");
|
|
1112
|
-
const num = parseInt(input.trim(), 10);
|
|
1113
|
-
if (num === 1) choice = 1;
|
|
1114
|
-
if (num === 2) choice = 2;
|
|
1115
|
-
}
|
|
1116
|
-
return choice === 1 ? "native" : "hosted";
|
|
1117
|
-
}
|
|
1118
1132
|
function getGeminiCliHooksInstallDir() {
|
|
1119
1133
|
return join(homedir(), ".multicorn", "gemini-cli-hooks");
|
|
1120
1134
|
}
|
|
@@ -1440,40 +1454,6 @@ async function installGeminiCliNativeHooks(ask) {
|
|
|
1440
1454
|
await mkdir(dirname(settingsPath), { recursive: true });
|
|
1441
1455
|
await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
1442
1456
|
}
|
|
1443
|
-
async function promptGeminiCliIntegrationMode(ask) {
|
|
1444
|
-
process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
|
|
1445
|
-
process.stderr.write(
|
|
1446
|
-
" " + style.violet("1") + ". Native plugin (recommended) - Gemini CLI Hooks see every file, terminal, web, and MCP action\n"
|
|
1447
|
-
);
|
|
1448
|
-
process.stderr.write(
|
|
1449
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Gemini CLI settings)\n"
|
|
1450
|
-
);
|
|
1451
|
-
let choice = 0;
|
|
1452
|
-
while (choice === 0) {
|
|
1453
|
-
const input = await ask("Choose integration (1-2): ");
|
|
1454
|
-
const num = parseInt(input.trim(), 10);
|
|
1455
|
-
if (num === 1) choice = 1;
|
|
1456
|
-
if (num === 2) choice = 2;
|
|
1457
|
-
}
|
|
1458
|
-
return choice === 1 ? "native" : "hosted";
|
|
1459
|
-
}
|
|
1460
|
-
async function promptOpencodeIntegrationMode(ask) {
|
|
1461
|
-
process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
|
|
1462
|
-
process.stderr.write(
|
|
1463
|
-
" " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
|
|
1464
|
-
);
|
|
1465
|
-
process.stderr.write(
|
|
1466
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
|
|
1467
|
-
);
|
|
1468
|
-
let choice = 0;
|
|
1469
|
-
while (choice === 0) {
|
|
1470
|
-
const input = await ask("Choose integration (1-2): ");
|
|
1471
|
-
const num = parseInt(input.trim(), 10);
|
|
1472
|
-
if (num === 1) choice = 1;
|
|
1473
|
-
if (num === 2) choice = 2;
|
|
1474
|
-
}
|
|
1475
|
-
return choice === 1 ? "native" : "hosted";
|
|
1476
|
-
}
|
|
1477
1457
|
function getClaudeDesktopConfigPath() {
|
|
1478
1458
|
switch (process.platform) {
|
|
1479
1459
|
case "win32":
|
|
@@ -1537,8 +1517,11 @@ function getClineMcpSettingsPath() {
|
|
|
1537
1517
|
);
|
|
1538
1518
|
}
|
|
1539
1519
|
}
|
|
1540
|
-
function
|
|
1541
|
-
|
|
1520
|
+
function resolveInitHybridIntegrationMode(explicit) {
|
|
1521
|
+
return explicit ?? "native";
|
|
1522
|
+
}
|
|
1523
|
+
function platformMenuLabelForPickerSelection(sel) {
|
|
1524
|
+
const slug = PICKER_SLUG_BY_SELECTION[sel];
|
|
1542
1525
|
if (slug === void 0) return "Unknown";
|
|
1543
1526
|
const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
|
|
1544
1527
|
return entry?.displayName ?? slug;
|
|
@@ -1556,48 +1539,31 @@ async function promptPlatformSelection(ask) {
|
|
|
1556
1539
|
process.stderr.write(
|
|
1557
1540
|
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
|
|
1558
1541
|
);
|
|
1542
|
+
process.stderr.write(" " + style.dim("Native plugin (files, terminal, browser)") + "\n");
|
|
1559
1543
|
let optionNum = 1;
|
|
1560
|
-
for (const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1544
|
+
for (const slug of INIT_WIZARD_PICKER_NATIVE_SLUGS) {
|
|
1545
|
+
const entry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === slug);
|
|
1546
|
+
const label = entry?.displayName ?? slug;
|
|
1547
|
+
const indent = optionNum >= 10 ? " " : " ";
|
|
1548
|
+
process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${label}
|
|
1565
1549
|
`);
|
|
1566
|
-
|
|
1567
|
-
}
|
|
1550
|
+
optionNum++;
|
|
1568
1551
|
}
|
|
1569
|
-
process.stderr.write(
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1552
|
+
process.stderr.write("\n");
|
|
1553
|
+
process.stderr.write(" MCP agents (Cursor, Copilot, others) are set up\n");
|
|
1554
|
+
process.stderr.write(` in the dashboard: ${INIT_WIZARD_DASHBOARD_AGENTS_URL}
|
|
1555
|
+
|
|
1556
|
+
`);
|
|
1574
1557
|
let selection = 0;
|
|
1575
1558
|
while (selection === 0) {
|
|
1576
|
-
const input = await ask(`Select (1-${String(
|
|
1559
|
+
const input = await ask(`Select (1-${String(INIT_WIZARD_PICKER_SELECTION_MAX)}): `);
|
|
1577
1560
|
const num = parseInt(input.trim(), 10);
|
|
1578
|
-
if (num >= 1 && num <=
|
|
1561
|
+
if (num >= 1 && num <= INIT_WIZARD_PICKER_SELECTION_MAX) {
|
|
1579
1562
|
selection = num;
|
|
1580
1563
|
}
|
|
1581
1564
|
}
|
|
1582
1565
|
return selection;
|
|
1583
1566
|
}
|
|
1584
|
-
async function promptWindsurfIntegrationMode(ask) {
|
|
1585
|
-
process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
|
|
1586
|
-
process.stderr.write(
|
|
1587
|
-
" " + style.violet("1") + ". Native plugin (recommended) - Cascade Hooks see every file, terminal, and MCP action\n"
|
|
1588
|
-
);
|
|
1589
|
-
process.stderr.write(
|
|
1590
|
-
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into mcp_config)\n"
|
|
1591
|
-
);
|
|
1592
|
-
let choice = 0;
|
|
1593
|
-
while (choice === 0) {
|
|
1594
|
-
const input = await ask("Choose integration (1-2): ");
|
|
1595
|
-
const num = parseInt(input.trim(), 10);
|
|
1596
|
-
if (num === 1) choice = 1;
|
|
1597
|
-
if (num === 2) choice = 2;
|
|
1598
|
-
}
|
|
1599
|
-
return choice === 1 ? "native" : "hosted";
|
|
1600
|
-
}
|
|
1601
1567
|
async function arrowSelect(options, ask, fallbackLabel) {
|
|
1602
1568
|
if (options.length === 1) {
|
|
1603
1569
|
const only = options[0] ?? "";
|
|
@@ -1801,6 +1767,14 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
|
|
|
1801
1767
|
function shouldEmbedKeyInHostedProxyUrl(platform) {
|
|
1802
1768
|
return HOSTED_PROXY_PLATFORMS_WITH_URL_KEY.has(platform);
|
|
1803
1769
|
}
|
|
1770
|
+
function buildClineHostedMcpEntry(url) {
|
|
1771
|
+
return {
|
|
1772
|
+
type: "streamableHttp",
|
|
1773
|
+
url,
|
|
1774
|
+
timeout: 60,
|
|
1775
|
+
disabled: false
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1804
1778
|
function hostedProxyUrlWithKeyParam(proxyUrl, apiKey) {
|
|
1805
1779
|
if (apiKey.length === 0) {
|
|
1806
1780
|
process.stderr.write(
|
|
@@ -2204,9 +2178,11 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
2204
2178
|
printHostedProxyJsonParseWarning(getWindsurfMcpConfigPath());
|
|
2205
2179
|
}
|
|
2206
2180
|
} else if (platform === "cline") {
|
|
2207
|
-
result = await mergeMcpServersObjectStyle(
|
|
2208
|
-
|
|
2209
|
-
|
|
2181
|
+
result = await mergeMcpServersObjectStyle(
|
|
2182
|
+
getClineMcpSettingsPath(),
|
|
2183
|
+
shortName,
|
|
2184
|
+
buildClineHostedMcpEntry(proxyUrlWithKeyWhenNeeded)
|
|
2185
|
+
);
|
|
2210
2186
|
if (result === "parse-error") {
|
|
2211
2187
|
printHostedProxyJsonParseWarning(getClineMcpSettingsPath());
|
|
2212
2188
|
}
|
|
@@ -2496,9 +2472,7 @@ Authorization = "Bearer ${bearerToken}"
|
|
|
2496
2472
|
snippetText = platform === "cline" ? JSON.stringify(
|
|
2497
2473
|
{
|
|
2498
2474
|
mcpServers: {
|
|
2499
|
-
[shortName]:
|
|
2500
|
-
[urlKey]: urlInSnippet
|
|
2501
|
-
}
|
|
2475
|
+
[shortName]: buildClineHostedMcpEntry(urlInSnippet)
|
|
2502
2476
|
}
|
|
2503
2477
|
},
|
|
2504
2478
|
null,
|
|
@@ -2661,6 +2635,7 @@ function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
|
|
|
2661
2635
|
}
|
|
2662
2636
|
async function runInit(explicitBaseUrl, options) {
|
|
2663
2637
|
const verbose = options?.verbose === true;
|
|
2638
|
+
const hybridIntegration = resolveInitHybridIntegrationMode(options?.integration);
|
|
2664
2639
|
if (!process.stdin.isTTY) {
|
|
2665
2640
|
process.stderr.write(
|
|
2666
2641
|
style.red("Error: interactive terminal required. Cannot run init with piped input.") + "\n"
|
|
@@ -2705,9 +2680,31 @@ async function runInit(explicitBaseUrl, options) {
|
|
|
2705
2680
|
if (existing !== null && existing.apiKey.startsWith("mcs_") && existing.apiKey.length >= 8) {
|
|
2706
2681
|
const masked = "mcs_..." + existing.apiKey.slice(-4);
|
|
2707
2682
|
process.stderr.write("Found existing API key: " + style.cyan(masked) + "\n");
|
|
2708
|
-
const answer = await ask("Use this key? (Y/n) ");
|
|
2709
|
-
|
|
2710
|
-
|
|
2683
|
+
const answer = await ask("Use this key? (Y/n, or paste a new mcs_ key) ");
|
|
2684
|
+
const trimmed = answer.trim();
|
|
2685
|
+
if (trimmed.toLowerCase() !== "n") {
|
|
2686
|
+
const candidate = isPastedShieldApiKeyInput(trimmed) ? trimmed : existing.apiKey;
|
|
2687
|
+
const spinner = withSpinner("Validating key...");
|
|
2688
|
+
let reuseResult;
|
|
2689
|
+
try {
|
|
2690
|
+
reuseResult = await validateApiKey(candidate, resolvedBaseUrl);
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
spinner.stop(false, "Validation failed");
|
|
2693
|
+
throw error;
|
|
2694
|
+
}
|
|
2695
|
+
if (reuseResult.valid) {
|
|
2696
|
+
spinner.stop(true, "Key validated");
|
|
2697
|
+
apiKey = candidate;
|
|
2698
|
+
} else {
|
|
2699
|
+
spinner.stop(false, reuseResult.error ?? "Validation failed.");
|
|
2700
|
+
if (isPastedShieldApiKeyInput(trimmed)) {
|
|
2701
|
+
process.stderr.write(style.red("That key was rejected. Enter a new key below.") + "\n");
|
|
2702
|
+
} else {
|
|
2703
|
+
process.stderr.write(
|
|
2704
|
+
style.yellow("Existing key is no longer valid. Enter a new key below.") + "\n"
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2711
2708
|
}
|
|
2712
2709
|
}
|
|
2713
2710
|
if (apiKey.length === 0) {
|
|
@@ -2759,8 +2756,8 @@ async function runInit(explicitBaseUrl, options) {
|
|
|
2759
2756
|
let removeAgentNameBeforeSave = void 0;
|
|
2760
2757
|
const initWorkspacePath = resolve(process.cwd());
|
|
2761
2758
|
const selection = await promptPlatformSelection(ask);
|
|
2762
|
-
const selectedPlatform =
|
|
2763
|
-
const selectedLabel =
|
|
2759
|
+
const selectedPlatform = PICKER_SLUG_BY_SELECTION[selection] ?? "openclaw";
|
|
2760
|
+
const selectedLabel = platformMenuLabelForPickerSelection(selection);
|
|
2764
2761
|
if (selectedPlatform === "other-mcp") {
|
|
2765
2762
|
const raw = existing !== null ? { ...existing } : {};
|
|
2766
2763
|
raw["apiKey"] = apiKey;
|
|
@@ -2894,7 +2891,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2894
2891
|
}
|
|
2895
2892
|
if (detection.status === "parse-error") {
|
|
2896
2893
|
process.stderr.write(
|
|
2897
|
-
style.red("\u2717") + " Could not update OpenClaw config. Set
|
|
2894
|
+
style.red("\u2717") + " Could not update OpenClaw config. Set plugins.entries.multicorn-shield.config.apiKey in ~/.openclaw/openclaw.json, or run npx multicorn-shield init.\n"
|
|
2898
2895
|
);
|
|
2899
2896
|
}
|
|
2900
2897
|
if (detection.status === "detected") {
|
|
@@ -2934,7 +2931,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2934
2931
|
if (result === "parse-error") {
|
|
2935
2932
|
spinner.stop(
|
|
2936
2933
|
false,
|
|
2937
|
-
"Could not update OpenClaw config. Set
|
|
2934
|
+
"Could not update OpenClaw config. Set plugins.entries.multicorn-shield.config.apiKey in ~/.openclaw/openclaw.json, or run npx multicorn-shield init."
|
|
2938
2935
|
);
|
|
2939
2936
|
} else {
|
|
2940
2937
|
spinner.stop(
|
|
@@ -2981,7 +2978,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2981
2978
|
});
|
|
2982
2979
|
setupSucceeded = true;
|
|
2983
2980
|
} else if (selectedPlatform === "windsurf") {
|
|
2984
|
-
const windsurfMode =
|
|
2981
|
+
const windsurfMode = hybridIntegration;
|
|
2985
2982
|
if (windsurfMode === "native") {
|
|
2986
2983
|
try {
|
|
2987
2984
|
await installWindsurfNativeHooks();
|
|
@@ -3080,7 +3077,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3080
3077
|
}
|
|
3081
3078
|
}
|
|
3082
3079
|
} else if (selectedPlatform === "gemini-cli") {
|
|
3083
|
-
const geminiMode =
|
|
3080
|
+
const geminiMode = hybridIntegration;
|
|
3084
3081
|
if (geminiMode === "native") {
|
|
3085
3082
|
try {
|
|
3086
3083
|
await installGeminiCliNativeHooks(ask);
|
|
@@ -3174,7 +3171,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3174
3171
|
}
|
|
3175
3172
|
}
|
|
3176
3173
|
} else if (selectedPlatform === "opencode") {
|
|
3177
|
-
const opencodeMode =
|
|
3174
|
+
const opencodeMode = hybridIntegration;
|
|
3178
3175
|
if (opencodeMode === "native") {
|
|
3179
3176
|
try {
|
|
3180
3177
|
await installOpenCodeNativePlugin();
|
|
@@ -3263,7 +3260,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3263
3260
|
}
|
|
3264
3261
|
}
|
|
3265
3262
|
} else if (selectedPlatform === "codex-cli") {
|
|
3266
|
-
const codexMode =
|
|
3263
|
+
const codexMode = hybridIntegration;
|
|
3267
3264
|
if (codexMode === "native") {
|
|
3268
3265
|
try {
|
|
3269
3266
|
await installCodexCliNativeHooks();
|
|
@@ -3352,7 +3349,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3352
3349
|
}
|
|
3353
3350
|
}
|
|
3354
3351
|
} else if (selectedPlatform === "cline") {
|
|
3355
|
-
const clineMode =
|
|
3352
|
+
const clineMode = hybridIntegration;
|
|
3356
3353
|
if (clineMode === "native") {
|
|
3357
3354
|
try {
|
|
3358
3355
|
await installClineNativeHooks();
|
|
@@ -3783,9 +3780,7 @@ async function writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, work
|
|
|
3783
3780
|
return await mergeMcpServersObjectStyle(
|
|
3784
3781
|
filePath,
|
|
3785
3782
|
entryKey,
|
|
3786
|
-
|
|
3787
|
-
url
|
|
3788
|
-
},
|
|
3783
|
+
buildClineHostedMcpEntry(url),
|
|
3789
3784
|
legacyKeys
|
|
3790
3785
|
) === "ok" ? filePath : null;
|
|
3791
3786
|
case "windsurf":
|
|
@@ -3946,7 +3941,7 @@ Authorization = "${authHeader}"
|
|
|
3946
3941
|
}
|
|
3947
3942
|
}
|
|
3948
3943
|
}
|
|
3949
|
-
var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, INIT_WIZARD_PLATFORM_REGISTRY,
|
|
3944
|
+
var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, OPENCLAW_SHIELD_API_KEY_ENV_REF, INIT_WIZARD_PLATFORM_REGISTRY, INIT_WIZARD_PICKER_NATIVE_SLUGS, INIT_WIZARD_DASHBOARD_AGENTS_URL, INIT_WIZARD_PICKER_SELECTION_MAX, PICKER_SLUG_BY_SELECTION, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL, CODING_CLIENTS, CLIENT_DISPLAY_NAMES, CLIENT_RELOAD_INSTRUCTIONS, CODING_CLIENT_TO_PLATFORM;
|
|
3950
3945
|
var init_config = __esm({
|
|
3951
3946
|
"src/proxy/config.ts"() {
|
|
3952
3947
|
init_consent();
|
|
@@ -3980,6 +3975,7 @@ var init_config = __esm({
|
|
|
3980
3975
|
ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
3981
3976
|
UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD = /^(Bearer|Basic|Token|ApiKey)(\s+)(.+)$/is;
|
|
3982
3977
|
OPENCLAW_MIN_VERSION = "2026.2.26";
|
|
3978
|
+
OPENCLAW_SHIELD_API_KEY_ENV_REF = "${MULTICORN_API_KEY}";
|
|
3983
3979
|
INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
3984
3980
|
{ slug: "openclaw", displayName: "OpenClaw", section: "native" },
|
|
3985
3981
|
{ slug: "claude-code", displayName: "Claude Code", section: "native" },
|
|
@@ -4036,25 +4032,17 @@ var init_config = __esm({
|
|
|
4036
4032
|
},
|
|
4037
4033
|
{ slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
|
|
4038
4034
|
];
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
{ title: "Recommended (native plugin)", items: itemsFor("native") },
|
|
4046
|
-
{ title: "Hosted proxy (MCP only)", items: itemsFor("hosted") }
|
|
4047
|
-
];
|
|
4048
|
-
})();
|
|
4049
|
-
INIT_WIZARD_SELECTION_MAX = INIT_WIZARD_PLATFORM_REGISTRY.length;
|
|
4035
|
+
INIT_WIZARD_PICKER_NATIVE_SLUGS = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === "native").map((e) => e.slug);
|
|
4036
|
+
INIT_WIZARD_DASHBOARD_AGENTS_URL = "https://app.multicorn.ai/agents/new";
|
|
4037
|
+
INIT_WIZARD_PICKER_SELECTION_MAX = INIT_WIZARD_PICKER_NATIVE_SLUGS.length;
|
|
4038
|
+
PICKER_SLUG_BY_SELECTION = Object.fromEntries(
|
|
4039
|
+
INIT_WIZARD_PICKER_NATIVE_SLUGS.map((slug, i) => [i + 1, slug])
|
|
4040
|
+
);
|
|
4050
4041
|
INIT_EXISTING_AGENTS_PLATFORM_ACTIONS = [
|
|
4051
4042
|
"Add a new agent alongside these",
|
|
4052
4043
|
"Replace an existing agent",
|
|
4053
4044
|
"Skip - choose a different platform"
|
|
4054
4045
|
];
|
|
4055
|
-
PLATFORM_BY_SELECTION = Object.fromEntries(
|
|
4056
|
-
INIT_WIZARD_PLATFORM_REGISTRY.map((e, i) => [i + 1, e.slug])
|
|
4057
|
-
);
|
|
4058
4046
|
HOSTED_PROXY_PLATFORMS_WITH_URL_KEY = /* @__PURE__ */ new Set([
|
|
4059
4047
|
"cursor",
|
|
4060
4048
|
"claude-desktop",
|
|
@@ -5062,7 +5050,7 @@ var init_package = __esm({
|
|
|
5062
5050
|
"package.json"() {
|
|
5063
5051
|
package_default = {
|
|
5064
5052
|
name: "multicorn-shield",
|
|
5065
|
-
version: "1.
|
|
5053
|
+
version: "1.12.0",
|
|
5066
5054
|
description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5067
5055
|
license: "MIT",
|
|
5068
5056
|
author: "Multicorn AI Pty Ltd",
|
|
@@ -5122,7 +5110,7 @@ var init_package = __esm({
|
|
|
5122
5110
|
node: ">=20"
|
|
5123
5111
|
},
|
|
5124
5112
|
scripts: {
|
|
5125
|
-
build: "tsup",
|
|
5113
|
+
build: "tsup && node scripts/stage-local-proxy-server.mjs",
|
|
5126
5114
|
dev: "tsup --watch",
|
|
5127
5115
|
lint: "eslint . --no-warn-ignored && prettier --check .",
|
|
5128
5116
|
"lint:fix": "eslint --fix . --no-warn-ignored && prettier --write .",
|
|
@@ -5141,7 +5129,8 @@ var init_package = __esm({
|
|
|
5141
5129
|
prepare: "husky || true",
|
|
5142
5130
|
"release:patch": "npm version patch && pnpm publish",
|
|
5143
5131
|
"release:minor": "npm version minor && pnpm publish",
|
|
5144
|
-
"release:major": "npm version major && pnpm publish"
|
|
5132
|
+
"release:major": "npm version major && pnpm publish",
|
|
5133
|
+
local: "pnpm build &&node dist/multicorn-shield.js init --base-url http://localhost:8080"
|
|
5145
5134
|
},
|
|
5146
5135
|
"lint-staged": {
|
|
5147
5136
|
"*.ts": [
|
|
@@ -5169,6 +5158,7 @@ var init_package = __esm({
|
|
|
5169
5158
|
"@size-limit/file": "^11.1.6",
|
|
5170
5159
|
"@types/node": "^22.0.0",
|
|
5171
5160
|
"@vitest/coverage-v8": "^3.0.5",
|
|
5161
|
+
ajv: "^8.18.0",
|
|
5172
5162
|
eslint: "^9.19.0",
|
|
5173
5163
|
"eslint-config-prettier": "^10.0.1",
|
|
5174
5164
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
@@ -5251,6 +5241,94 @@ var init_package_meta = __esm({
|
|
|
5251
5241
|
PACKAGE_VERSION = package_default.version;
|
|
5252
5242
|
}
|
|
5253
5243
|
});
|
|
5244
|
+
function findMulticornShieldPackageRoot(moduleDir = dirname(fileURLToPath(import.meta.url))) {
|
|
5245
|
+
let current = moduleDir;
|
|
5246
|
+
for (; ; ) {
|
|
5247
|
+
const pkgPath = join(current, "package.json");
|
|
5248
|
+
if (existsSync(pkgPath)) {
|
|
5249
|
+
try {
|
|
5250
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
5251
|
+
if (pkg.name === "multicorn-shield") {
|
|
5252
|
+
return current;
|
|
5253
|
+
}
|
|
5254
|
+
} catch {
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
const parent = dirname(current);
|
|
5258
|
+
if (parent === current) {
|
|
5259
|
+
break;
|
|
5260
|
+
}
|
|
5261
|
+
current = parent;
|
|
5262
|
+
}
|
|
5263
|
+
try {
|
|
5264
|
+
const req = createRequire(import.meta.url);
|
|
5265
|
+
return dirname(req.resolve("multicorn-shield/package.json"));
|
|
5266
|
+
} catch {
|
|
5267
|
+
throw new Error(NOT_FOUND_MESSAGE);
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
function resolveLocalProxyServerEntry(options) {
|
|
5271
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
5272
|
+
const candidates = [
|
|
5273
|
+
join(findMulticornShieldPackageRoot(moduleDir), "dist", "server.js")
|
|
5274
|
+
];
|
|
5275
|
+
try {
|
|
5276
|
+
const req = createRequire(import.meta.url);
|
|
5277
|
+
const proxyRoot = dirname(req.resolve("multicorn-proxy/package.json"));
|
|
5278
|
+
candidates.unshift(join(proxyRoot, "dist", "server.js"));
|
|
5279
|
+
} catch {
|
|
5280
|
+
}
|
|
5281
|
+
for (const path of candidates) {
|
|
5282
|
+
if (existsSync(path)) return path;
|
|
5283
|
+
}
|
|
5284
|
+
throw new Error(NOT_FOUND_MESSAGE);
|
|
5285
|
+
}
|
|
5286
|
+
function buildLocalProxySpawnEnv(port, apiBaseUrl) {
|
|
5287
|
+
return {
|
|
5288
|
+
PORT: String(port),
|
|
5289
|
+
HOST: "127.0.0.1",
|
|
5290
|
+
SHIELD_API_BASE_URL: apiBaseUrl,
|
|
5291
|
+
ALLOW_PRIVATE_TARGETS: "true"
|
|
5292
|
+
};
|
|
5293
|
+
}
|
|
5294
|
+
function buildLocalProxySpawnCommand(port, apiBaseUrl, execPath = process.execPath, serverEntryPath = resolveLocalProxyServerEntry()) {
|
|
5295
|
+
const env = buildLocalProxySpawnEnv(port, apiBaseUrl);
|
|
5296
|
+
return {
|
|
5297
|
+
executable: execPath,
|
|
5298
|
+
args: [serverEntryPath],
|
|
5299
|
+
env,
|
|
5300
|
+
serverEntryPath
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
function readProxyLogTail(logPath, maxBytes = 8e3) {
|
|
5304
|
+
try {
|
|
5305
|
+
const content = readFileSync(logPath, "utf8");
|
|
5306
|
+
if (content.length <= maxBytes) return content;
|
|
5307
|
+
return content.slice(-maxBytes);
|
|
5308
|
+
} catch {
|
|
5309
|
+
return "";
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
function formatLocalProxyStartError(port, logPath, childExited, exitCode) {
|
|
5313
|
+
const logTail = readProxyLogTail(logPath);
|
|
5314
|
+
const reason = childExited ? `Proxy process exited early${exitCode === null ? "" : ` (code ${String(exitCode)})`}.` : `Proxy did not become ready on port ${String(port)} within the timeout.`;
|
|
5315
|
+
let message = `Could not start the local proxy on port ${String(port)}. ${reason}`;
|
|
5316
|
+
message += logTail.length > 0 ? `
|
|
5317
|
+
|
|
5318
|
+
Output from ${logPath}:
|
|
5319
|
+
${logTail.trimEnd()}` : `
|
|
5320
|
+
|
|
5321
|
+
See ${logPath} for details.`;
|
|
5322
|
+
return message;
|
|
5323
|
+
}
|
|
5324
|
+
var LOCAL_PROXY_READY_POLL_MS, LOCAL_PROXY_READY_MAX_POLLS, NOT_FOUND_MESSAGE;
|
|
5325
|
+
var init_local_proxy_start = __esm({
|
|
5326
|
+
"src/commands/local-proxy-start.ts"() {
|
|
5327
|
+
LOCAL_PROXY_READY_POLL_MS = 500;
|
|
5328
|
+
LOCAL_PROXY_READY_MAX_POLLS = 30;
|
|
5329
|
+
NOT_FOUND_MESSAGE = "Local proxy server entry (dist/server.js) not found. Reinstall multicorn-shield or run pnpm build.";
|
|
5330
|
+
}
|
|
5331
|
+
});
|
|
5254
5332
|
function pidfilePath(agent) {
|
|
5255
5333
|
return join(PIDFILE_DIR, `files-${agent}.pid`);
|
|
5256
5334
|
}
|
|
@@ -5270,6 +5348,16 @@ function readPidfile(agent) {
|
|
|
5270
5348
|
return null;
|
|
5271
5349
|
}
|
|
5272
5350
|
}
|
|
5351
|
+
function supervisorPidFromSession(data) {
|
|
5352
|
+
if (typeof data.supervisorPid === "number") return data.supervisorPid;
|
|
5353
|
+
if (typeof data.pid === "number") return data.pid;
|
|
5354
|
+
return void 0;
|
|
5355
|
+
}
|
|
5356
|
+
function isAgentSessionRunning(data) {
|
|
5357
|
+
if (data === null) return false;
|
|
5358
|
+
const pid = supervisorPidFromSession(data);
|
|
5359
|
+
return typeof pid === "number" && isProcessAlive(pid);
|
|
5360
|
+
}
|
|
5273
5361
|
function removePidfile(agent) {
|
|
5274
5362
|
const p = pidfilePath(agent);
|
|
5275
5363
|
try {
|
|
@@ -5310,7 +5398,7 @@ function runStatus() {
|
|
|
5310
5398
|
const fsReg = readFsRegistry();
|
|
5311
5399
|
process.stderr.write("Active sessions:\n\n");
|
|
5312
5400
|
for (const s of sessions) {
|
|
5313
|
-
const supervisorAlive =
|
|
5401
|
+
const supervisorAlive = isAgentSessionRunning(s);
|
|
5314
5402
|
const fsEntry = fsReg[s.dir];
|
|
5315
5403
|
const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
|
|
5316
5404
|
const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
|
|
@@ -5457,9 +5545,7 @@ async function withResourceLock(fn) {
|
|
|
5457
5545
|
}
|
|
5458
5546
|
}
|
|
5459
5547
|
function liveAgents() {
|
|
5460
|
-
return listAllPidfiles().filter(
|
|
5461
|
-
(p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
|
|
5462
|
-
);
|
|
5548
|
+
return listAllPidfiles().filter((p) => isAgentSessionRunning(p));
|
|
5463
5549
|
}
|
|
5464
5550
|
function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
|
|
5465
5551
|
return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
|
|
@@ -5477,27 +5563,37 @@ async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE)
|
|
|
5477
5563
|
`No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
|
|
5478
5564
|
);
|
|
5479
5565
|
}
|
|
5566
|
+
async function resolveBaseUrl(explicitBaseUrl) {
|
|
5567
|
+
if (explicitBaseUrl !== void 0 && explicitBaseUrl.trim().length > 0) {
|
|
5568
|
+
return explicitBaseUrl.trim();
|
|
5569
|
+
}
|
|
5570
|
+
const envBaseUrl = process.env["MULTICORN_BASE_URL"];
|
|
5571
|
+
if (typeof envBaseUrl === "string" && envBaseUrl.trim().length > 0) {
|
|
5572
|
+
return envBaseUrl.trim();
|
|
5573
|
+
}
|
|
5574
|
+
const config = await loadConfig();
|
|
5575
|
+
if (config !== null && config.baseUrl.length > 0) {
|
|
5576
|
+
return config.baseUrl;
|
|
5577
|
+
}
|
|
5578
|
+
const fromFile = await readBaseUrlFromConfig();
|
|
5579
|
+
if (fromFile !== void 0 && fromFile.length > 0) {
|
|
5580
|
+
return fromFile;
|
|
5581
|
+
}
|
|
5582
|
+
return DEFAULT_SHIELD_API_BASE_URL;
|
|
5583
|
+
}
|
|
5480
5584
|
async function resolveConfig(opts) {
|
|
5585
|
+
const baseUrl = await resolveBaseUrl(opts.baseUrl);
|
|
5481
5586
|
const fromFlag = opts.apiKey;
|
|
5482
5587
|
if (fromFlag && fromFlag.length > 0) {
|
|
5483
|
-
return {
|
|
5484
|
-
apiKey: fromFlag,
|
|
5485
|
-
baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
|
|
5486
|
-
};
|
|
5588
|
+
return { apiKey: fromFlag, baseUrl };
|
|
5487
5589
|
}
|
|
5488
5590
|
const envKey = process.env["MULTICORN_API_KEY"];
|
|
5489
5591
|
if (typeof envKey === "string" && envKey.length > 0) {
|
|
5490
|
-
return {
|
|
5491
|
-
apiKey: envKey,
|
|
5492
|
-
baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
|
|
5493
|
-
};
|
|
5592
|
+
return { apiKey: envKey, baseUrl };
|
|
5494
5593
|
}
|
|
5495
5594
|
const config = await loadConfig();
|
|
5496
5595
|
if (config !== null) {
|
|
5497
|
-
return {
|
|
5498
|
-
apiKey: config.apiKey,
|
|
5499
|
-
baseUrl: opts.baseUrl ?? config.baseUrl
|
|
5500
|
-
};
|
|
5596
|
+
return { apiKey: config.apiKey, baseUrl };
|
|
5501
5597
|
}
|
|
5502
5598
|
process.stderr.write(
|
|
5503
5599
|
"No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
|
|
@@ -5537,26 +5633,40 @@ function startLocalProxyDetached(port, apiBaseUrl) {
|
|
|
5537
5633
|
const logFile = join(PIDFILE_DIR, "proxy.log");
|
|
5538
5634
|
const out = openSync(logFile, "a");
|
|
5539
5635
|
const err = openSync(logFile, "a");
|
|
5540
|
-
const
|
|
5636
|
+
const { executable, args, env: proxyEnv } = buildLocalProxySpawnCommand(port, apiBaseUrl);
|
|
5637
|
+
const child = spawn(executable, [...args], {
|
|
5541
5638
|
stdio: ["ignore", out, err],
|
|
5542
5639
|
detached: true,
|
|
5543
5640
|
env: {
|
|
5544
5641
|
...process.env,
|
|
5545
|
-
|
|
5546
|
-
HOST: "127.0.0.1",
|
|
5547
|
-
SHIELD_API_BASE_URL: apiBaseUrl,
|
|
5642
|
+
...proxyEnv
|
|
5548
5643
|
// SAFETY: blanket-allow for private targets is acceptable ONLY because
|
|
5549
5644
|
// this is a local single-user proxy. The only registered target is the
|
|
5550
5645
|
// filesystem server on the same machine. Do NOT copy this pattern to a
|
|
5551
5646
|
// multi-tenant or hosted proxy deployment.
|
|
5552
|
-
ALLOW_PRIVATE_TARGETS: "true"
|
|
5553
5647
|
}
|
|
5554
5648
|
});
|
|
5555
5649
|
child.unref();
|
|
5556
5650
|
return child;
|
|
5557
5651
|
}
|
|
5558
|
-
async function ensureProxy(proxyPort, apiBaseUrl) {
|
|
5559
|
-
|
|
5652
|
+
async function ensureProxy(proxyPort, apiBaseUrl, options) {
|
|
5653
|
+
const forceRespawn = options?.forceRespawn === true;
|
|
5654
|
+
if (forceRespawn) {
|
|
5655
|
+
const reg2 = readProxyRegistry();
|
|
5656
|
+
if (reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid)) {
|
|
5657
|
+
killWithEscalation(reg2.pid, true);
|
|
5658
|
+
}
|
|
5659
|
+
if (reg2 !== null && reg2.port === proxyPort) {
|
|
5660
|
+
try {
|
|
5661
|
+
unlinkSync(PROXY_REGISTRY);
|
|
5662
|
+
} catch {
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
for (let i = 0; i < 20; i++) {
|
|
5666
|
+
if (!await isPortListening(proxyPort)) break;
|
|
5667
|
+
await sleep3(100);
|
|
5668
|
+
}
|
|
5669
|
+
} else if (await probeProxyHealth(proxyPort)) {
|
|
5560
5670
|
const reg2 = readProxyRegistry();
|
|
5561
5671
|
const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
|
|
5562
5672
|
return { reused: true, managed };
|
|
@@ -5573,9 +5683,11 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
|
|
|
5573
5683
|
`A server is already running on port ${String(proxyPort)} but it's not a healthy Shield proxy. Stop it or re-run with --proxy-port <n>.`
|
|
5574
5684
|
);
|
|
5575
5685
|
}
|
|
5686
|
+
const logFile = join(PIDFILE_DIR, "proxy.log");
|
|
5576
5687
|
const child = startLocalProxyDetached(proxyPort, apiBaseUrl);
|
|
5577
|
-
for (let i = 0; i <
|
|
5578
|
-
await sleep3(
|
|
5688
|
+
for (let i = 0; i < LOCAL_PROXY_READY_MAX_POLLS; i++) {
|
|
5689
|
+
await sleep3(LOCAL_PROXY_READY_POLL_MS);
|
|
5690
|
+
if (child.exitCode !== null || child.signalCode !== null) break;
|
|
5579
5691
|
if (await probeProxyHealth(proxyPort)) {
|
|
5580
5692
|
if (child.pid !== void 0) {
|
|
5581
5693
|
writeJsonFile(PROXY_REGISTRY, { pid: child.pid, port: proxyPort });
|
|
@@ -5587,9 +5699,8 @@ async function ensureProxy(proxyPort, apiBaseUrl) {
|
|
|
5587
5699
|
if (child.pid !== void 0) killWithEscalation(child.pid, true);
|
|
5588
5700
|
} catch {
|
|
5589
5701
|
}
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
);
|
|
5702
|
+
const childExited = child.exitCode !== null || child.signalCode !== null;
|
|
5703
|
+
throw new Error(formatLocalProxyStartError(proxyPort, logFile, childExited, child.exitCode));
|
|
5593
5704
|
}
|
|
5594
5705
|
async function ensureFsServer(realDir, requestedPort) {
|
|
5595
5706
|
const reg = readFsRegistry();
|
|
@@ -5791,8 +5902,9 @@ async function runStop(agent) {
|
|
|
5791
5902
|
process.exit(1);
|
|
5792
5903
|
}
|
|
5793
5904
|
await withResourceLock(() => {
|
|
5794
|
-
|
|
5795
|
-
|
|
5905
|
+
const supervisorPid = supervisorPidFromSession(data);
|
|
5906
|
+
if (typeof supervisorPid === "number" && isProcessAlive(supervisorPid)) {
|
|
5907
|
+
killWithEscalation(supervisorPid);
|
|
5796
5908
|
}
|
|
5797
5909
|
removePidfile(agent);
|
|
5798
5910
|
releaseFsServerIfUnused(data.dir, agent);
|
|
@@ -5822,7 +5934,8 @@ Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${
|
|
|
5822
5934
|
await runDetached({
|
|
5823
5935
|
...opts,
|
|
5824
5936
|
dir,
|
|
5825
|
-
proxyPort: opts.proxyPort ?? existing?.proxyPort
|
|
5937
|
+
proxyPort: opts.proxyPort ?? existing?.proxyPort,
|
|
5938
|
+
respawnProxy: true});
|
|
5826
5939
|
}
|
|
5827
5940
|
async function runDetached(opts) {
|
|
5828
5941
|
const absDir = resolve(opts.dir);
|
|
@@ -5833,8 +5946,7 @@ async function runDetached(opts) {
|
|
|
5833
5946
|
}
|
|
5834
5947
|
const existing = readPidfile(opts.agent);
|
|
5835
5948
|
if (existing !== null) {
|
|
5836
|
-
|
|
5837
|
-
if (alive) {
|
|
5949
|
+
if (isAgentSessionRunning(existing)) {
|
|
5838
5950
|
process.stderr.write(
|
|
5839
5951
|
`Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
|
|
5840
5952
|
Stop with: npx multicorn-shield files stop --agent ${opts.agent}
|
|
@@ -5849,6 +5961,7 @@ Stop with: npx multicorn-shield files stop --agent ${opts.agent}
|
|
|
5849
5961
|
if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
|
|
5850
5962
|
if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
|
|
5851
5963
|
if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
|
|
5964
|
+
if (opts.respawnProxy) args.push("--respawn-proxy");
|
|
5852
5965
|
if (opts.client !== void 0) args.push("--client", opts.client);
|
|
5853
5966
|
const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
|
|
5854
5967
|
const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
|
|
@@ -5932,8 +6045,7 @@ async function runFilesCommand(opts) {
|
|
|
5932
6045
|
const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
|
|
5933
6046
|
const existingPidfile = readPidfile(opts.agent);
|
|
5934
6047
|
if (existingPidfile !== null) {
|
|
5935
|
-
|
|
5936
|
-
if (supervisorAlive) {
|
|
6048
|
+
if (isAgentSessionRunning(existingPidfile)) {
|
|
5937
6049
|
process.stderr.write(
|
|
5938
6050
|
`A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
|
|
5939
6051
|
`
|
|
@@ -5947,7 +6059,9 @@ async function runFilesCommand(opts) {
|
|
|
5947
6059
|
let fsReused;
|
|
5948
6060
|
try {
|
|
5949
6061
|
const ensured = await withResourceLock(async () => {
|
|
5950
|
-
const proxyRes = await ensureProxy(proxyPort, config.baseUrl
|
|
6062
|
+
const proxyRes = await ensureProxy(proxyPort, config.baseUrl, {
|
|
6063
|
+
forceRespawn: opts.respawnProxy
|
|
6064
|
+
});
|
|
5951
6065
|
const fsRes = await ensureFsServer(realDir, opts.port);
|
|
5952
6066
|
writePidfile({
|
|
5953
6067
|
agent: opts.agent,
|
|
@@ -6196,6 +6310,7 @@ var DEFAULT_FS_PORT, DEFAULT_PROXY_PORT, PIDFILE_DIR, PROXY_REGISTRY, FS_REGISTR
|
|
|
6196
6310
|
var init_files = __esm({
|
|
6197
6311
|
"src/commands/files.ts"() {
|
|
6198
6312
|
init_config();
|
|
6313
|
+
init_local_proxy_start();
|
|
6199
6314
|
DEFAULT_FS_PORT = 3005;
|
|
6200
6315
|
DEFAULT_PROXY_PORT = 3001;
|
|
6201
6316
|
PIDFILE_DIR = process.env["MULTICORN_HOME"] ?? join(homedir(), ".multicorn");
|
|
@@ -6242,6 +6357,8 @@ function parseArgs(argv) {
|
|
|
6242
6357
|
let filesForeground = false;
|
|
6243
6358
|
let filesStatus = false;
|
|
6244
6359
|
let filesRestart = false;
|
|
6360
|
+
let filesRespawnProxy = false;
|
|
6361
|
+
let initIntegration = void 0;
|
|
6245
6362
|
for (let i = 0; i < args.length; i++) {
|
|
6246
6363
|
const arg = args[i];
|
|
6247
6364
|
if (arg === "init") {
|
|
@@ -6292,6 +6409,8 @@ function parseArgs(argv) {
|
|
|
6292
6409
|
filesStop = true;
|
|
6293
6410
|
} else if (token === "--foreground") {
|
|
6294
6411
|
filesForeground = true;
|
|
6412
|
+
} else if (token === "--respawn-proxy") {
|
|
6413
|
+
filesRespawnProxy = true;
|
|
6295
6414
|
} else if (token === "--detach") ; else if (token === "stop") {
|
|
6296
6415
|
filesStop = true;
|
|
6297
6416
|
} else if (token === "status") {
|
|
@@ -6400,6 +6519,12 @@ function parseArgs(argv) {
|
|
|
6400
6519
|
}
|
|
6401
6520
|
} else if (arg === "--verbose" || arg === "--debug") {
|
|
6402
6521
|
verbose = true;
|
|
6522
|
+
} else if (arg === "--integration") {
|
|
6523
|
+
const next = args[i + 1];
|
|
6524
|
+
if (next === "native" || next === "hosted") {
|
|
6525
|
+
initIntegration = next;
|
|
6526
|
+
i++;
|
|
6527
|
+
}
|
|
6403
6528
|
}
|
|
6404
6529
|
}
|
|
6405
6530
|
return {
|
|
@@ -6413,6 +6538,7 @@ function parseArgs(argv) {
|
|
|
6413
6538
|
deleteAgentName,
|
|
6414
6539
|
apiKey,
|
|
6415
6540
|
verbose,
|
|
6541
|
+
initIntegration,
|
|
6416
6542
|
filesDir,
|
|
6417
6543
|
filesPort,
|
|
6418
6544
|
filesProxyPort,
|
|
@@ -6420,7 +6546,8 @@ function parseArgs(argv) {
|
|
|
6420
6546
|
filesClient,
|
|
6421
6547
|
filesForeground,
|
|
6422
6548
|
filesStatus,
|
|
6423
|
-
filesRestart
|
|
6549
|
+
filesRestart,
|
|
6550
|
+
filesRespawnProxy
|
|
6424
6551
|
};
|
|
6425
6552
|
}
|
|
6426
6553
|
function printHelp() {
|
|
@@ -6430,7 +6557,12 @@ function printHelp() {
|
|
|
6430
6557
|
"",
|
|
6431
6558
|
"Usage:",
|
|
6432
6559
|
" npx multicorn-shield init",
|
|
6433
|
-
" Interactive setup
|
|
6560
|
+
" Interactive setup for native plugins (files, terminal, browser).",
|
|
6561
|
+
" Native: OpenClaw, Claude Code, Windsurf, Cline, Gemini CLI, OpenCode, Codex CLI.",
|
|
6562
|
+
" MCP-only clients (Cursor, Copilot, etc.): https://app.multicorn.ai/agents/new",
|
|
6563
|
+
" Saves API key to ~/.multicorn/config.json.",
|
|
6564
|
+
" --integration hosted Use hosted MCP proxy for Windsurf, Cline, Gemini CLI,",
|
|
6565
|
+
" OpenCode, or Codex CLI (default: native).",
|
|
6434
6566
|
"",
|
|
6435
6567
|
" npx multicorn-shield files <dir> --agent <name> [--client <client>]",
|
|
6436
6568
|
" Share a local folder with a coding agent. Starts a filesystem MCP server",
|
|
@@ -6511,7 +6643,10 @@ async function runCli() {
|
|
|
6511
6643
|
process.exit(0);
|
|
6512
6644
|
}
|
|
6513
6645
|
if (cli.subcommand === "init") {
|
|
6514
|
-
await runInit(cli.baseUrl, {
|
|
6646
|
+
await runInit(cli.baseUrl, {
|
|
6647
|
+
verbose: cli.verbose,
|
|
6648
|
+
...cli.initIntegration !== void 0 ? { integration: cli.initIntegration } : {}
|
|
6649
|
+
});
|
|
6515
6650
|
return;
|
|
6516
6651
|
}
|
|
6517
6652
|
if (cli.subcommand === "files") {
|
|
@@ -6527,7 +6662,8 @@ async function runCli() {
|
|
|
6527
6662
|
client: void 0,
|
|
6528
6663
|
foreground: true,
|
|
6529
6664
|
status: true,
|
|
6530
|
-
restart: false
|
|
6665
|
+
restart: false,
|
|
6666
|
+
respawnProxy: false
|
|
6531
6667
|
});
|
|
6532
6668
|
return;
|
|
6533
6669
|
}
|
|
@@ -6552,7 +6688,8 @@ async function runCli() {
|
|
|
6552
6688
|
client: cli.filesClient,
|
|
6553
6689
|
foreground: cli.filesForeground,
|
|
6554
6690
|
status: false,
|
|
6555
|
-
restart: cli.filesRestart
|
|
6691
|
+
restart: cli.filesRestart,
|
|
6692
|
+
respawnProxy: cli.filesRespawnProxy
|
|
6556
6693
|
});
|
|
6557
6694
|
return;
|
|
6558
6695
|
}
|