multicorn-shield 1.11.1 → 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.
@@ -644,7 +644,34 @@ function isVersionAtLeast(version, minimum) {
644
644
  }
645
645
  return true;
646
646
  }
647
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
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,49 +687,66 @@ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
660
687
  } catch {
661
688
  return "parse-error";
662
689
  }
663
- let hooks = obj["hooks"];
664
- if (hooks === void 0 || typeof hooks !== "object") {
665
- hooks = {};
666
- obj["hooks"] = hooks;
690
+ if (detectStaleOpenClawHookIntegration(obj)) {
691
+ process.stderr.write(
692
+ style.yellow("\u26A0") + " " + staleOpenClawHookIntegrationMessage() + "\n"
693
+ );
667
694
  }
668
- let internal = hooks["internal"];
669
- if (internal === void 0 || typeof internal !== "object") {
670
- internal = { enabled: true, entries: {} };
671
- hooks["internal"] = internal;
695
+ let plugins = obj["plugins"];
696
+ if (plugins === void 0 || typeof plugins !== "object") {
697
+ plugins = {};
698
+ obj["plugins"] = plugins;
672
699
  }
673
- let entries = internal["entries"];
674
- if (entries === void 0 || typeof entries !== "object") {
675
- entries = {};
676
- internal["entries"] = entries;
700
+ let pluginEntries = plugins["entries"];
701
+ if (pluginEntries === void 0 || typeof pluginEntries !== "object") {
702
+ pluginEntries = {};
703
+ plugins["entries"] = pluginEntries;
677
704
  }
678
- let shield = entries["multicorn-shield"];
705
+ let shield = pluginEntries["multicorn-shield"];
679
706
  if (shield === void 0 || typeof shield !== "object") {
680
- shield = { enabled: true, env: {} };
681
- entries["multicorn-shield"] = shield;
682
- }
683
- let env = shield["env"];
684
- if (env === void 0 || typeof env !== "object") {
685
- env = {};
686
- shield["env"] = env;
687
- }
688
- env["MULTICORN_API_KEY"] = apiKey;
689
- env["MULTICORN_BASE_URL"] = baseUrl;
690
- if (agentName !== void 0) {
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"] !== agentName) {
697
- first["id"] = agentName;
698
- first["name"] = agentName;
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: agentName, name: agentName }];
728
+ agentsList["list"] = [{ id: resolvedAgentName, name: resolvedAgentName }];
703
729
  } else {
704
- obj["agents"] = { list: [{ id: agentName, name: agentName }] };
730
+ obj["agents"] = { list: [{ id: resolvedAgentName, name: resolvedAgentName }] };
731
+ }
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";
705
742
  }
743
+ shield["config"] = cfg;
744
+ } else {
745
+ shield["config"] = {
746
+ apiKey: OPENCLAW_SHIELD_API_KEY_ENV_REF,
747
+ baseUrl,
748
+ failMode: "closed"
749
+ };
706
750
  }
707
751
  }
708
752
  await writeFile(
@@ -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 platformMenuLabelForSelection(sel) {
1541
- const slug = PLATFORM_BY_SELECTION[sel];
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 section of INIT_WIZARD_MENU_SECTIONS) {
1561
- process.stderr.write(" " + style.dim(section.title) + "\n");
1562
- for (const item of section.items) {
1563
- const indent = optionNum >= 10 ? " " : " ";
1564
- process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${item.label}
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
- optionNum++;
1567
- }
1550
+ optionNum++;
1568
1551
  }
1569
- process.stderr.write(
1570
- "\n" + style.dim(
1571
- ` Pick ${String(INIT_WIZARD_SELECTION_MAX)} to wrap a local MCP server with multicorn-shield --wrap.`
1572
- ) + "\n"
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(INIT_WIZARD_SELECTION_MAX)}): `);
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 <= INIT_WIZARD_SELECTION_MAX) {
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(getClineMcpSettingsPath(), shortName, {
2208
- url: proxyUrlWithKeyWhenNeeded
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
- if (answer.trim().toLowerCase() !== "n") {
2710
- apiKey = existing.apiKey;
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 = PLATFORM_BY_SELECTION[selection] ?? "cursor";
2763
- const selectedLabel = platformMenuLabelForSelection(selection);
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 MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually.\n"
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 MULTICORN_API_KEY in ~/.openclaw/openclaw.json manually."
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 = await promptWindsurfIntegrationMode(ask);
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 = await promptGeminiCliIntegrationMode(ask);
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 = await promptOpencodeIntegrationMode(ask);
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 = await promptCodexCliIntegrationMode(ask);
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 = await promptClineIntegrationMode(ask);
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, INIT_WIZARD_MENU_SECTIONS, INIT_WIZARD_SELECTION_MAX, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, PLATFORM_BY_SELECTION, 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;
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
- INIT_WIZARD_MENU_SECTIONS = (() => {
4040
- const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
4041
- platform: e.slug,
4042
- label: e.displayName
4043
- }));
4044
- return [
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.11.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",
@@ -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",
@@ -5358,6 +5348,16 @@ function readPidfile(agent) {
5358
5348
  return null;
5359
5349
  }
5360
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
+ }
5361
5361
  function removePidfile(agent) {
5362
5362
  const p = pidfilePath(agent);
5363
5363
  try {
@@ -5398,7 +5398,7 @@ function runStatus() {
5398
5398
  const fsReg = readFsRegistry();
5399
5399
  process.stderr.write("Active sessions:\n\n");
5400
5400
  for (const s of sessions) {
5401
- const supervisorAlive = typeof s.supervisorPid === "number" && isProcessAlive(s.supervisorPid);
5401
+ const supervisorAlive = isAgentSessionRunning(s);
5402
5402
  const fsEntry = fsReg[s.dir];
5403
5403
  const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
5404
5404
  const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
@@ -5545,9 +5545,7 @@ async function withResourceLock(fn) {
5545
5545
  }
5546
5546
  }
5547
5547
  function liveAgents() {
5548
- return listAllPidfiles().filter(
5549
- (p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
5550
- );
5548
+ return listAllPidfiles().filter((p) => isAgentSessionRunning(p));
5551
5549
  }
5552
5550
  function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
5553
5551
  return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
@@ -5565,27 +5563,37 @@ async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE)
5565
5563
  `No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
5566
5564
  );
5567
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
+ }
5568
5584
  async function resolveConfig(opts) {
5585
+ const baseUrl = await resolveBaseUrl(opts.baseUrl);
5569
5586
  const fromFlag = opts.apiKey;
5570
5587
  if (fromFlag && fromFlag.length > 0) {
5571
- return {
5572
- apiKey: fromFlag,
5573
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5574
- };
5588
+ return { apiKey: fromFlag, baseUrl };
5575
5589
  }
5576
5590
  const envKey = process.env["MULTICORN_API_KEY"];
5577
5591
  if (typeof envKey === "string" && envKey.length > 0) {
5578
- return {
5579
- apiKey: envKey,
5580
- baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5581
- };
5592
+ return { apiKey: envKey, baseUrl };
5582
5593
  }
5583
5594
  const config = await loadConfig();
5584
5595
  if (config !== null) {
5585
- return {
5586
- apiKey: config.apiKey,
5587
- baseUrl: opts.baseUrl ?? config.baseUrl
5588
- };
5596
+ return { apiKey: config.apiKey, baseUrl };
5589
5597
  }
5590
5598
  process.stderr.write(
5591
5599
  "No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
@@ -5641,8 +5649,24 @@ function startLocalProxyDetached(port, apiBaseUrl) {
5641
5649
  child.unref();
5642
5650
  return child;
5643
5651
  }
5644
- async function ensureProxy(proxyPort, apiBaseUrl) {
5645
- if (await probeProxyHealth(proxyPort)) {
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)) {
5646
5670
  const reg2 = readProxyRegistry();
5647
5671
  const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
5648
5672
  return { reused: true, managed };
@@ -5878,8 +5902,9 @@ async function runStop(agent) {
5878
5902
  process.exit(1);
5879
5903
  }
5880
5904
  await withResourceLock(() => {
5881
- if (typeof data.supervisorPid === "number" && isProcessAlive(data.supervisorPid)) {
5882
- killWithEscalation(data.supervisorPid);
5905
+ const supervisorPid = supervisorPidFromSession(data);
5906
+ if (typeof supervisorPid === "number" && isProcessAlive(supervisorPid)) {
5907
+ killWithEscalation(supervisorPid);
5883
5908
  }
5884
5909
  removePidfile(agent);
5885
5910
  releaseFsServerIfUnused(data.dir, agent);
@@ -5909,7 +5934,8 @@ Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${
5909
5934
  await runDetached({
5910
5935
  ...opts,
5911
5936
  dir,
5912
- proxyPort: opts.proxyPort ?? existing?.proxyPort});
5937
+ proxyPort: opts.proxyPort ?? existing?.proxyPort,
5938
+ respawnProxy: true});
5913
5939
  }
5914
5940
  async function runDetached(opts) {
5915
5941
  const absDir = resolve(opts.dir);
@@ -5920,8 +5946,7 @@ async function runDetached(opts) {
5920
5946
  }
5921
5947
  const existing = readPidfile(opts.agent);
5922
5948
  if (existing !== null) {
5923
- const alive = typeof existing.supervisorPid === "number" && isProcessAlive(existing.supervisorPid);
5924
- if (alive) {
5949
+ if (isAgentSessionRunning(existing)) {
5925
5950
  process.stderr.write(
5926
5951
  `Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
5927
5952
  Stop with: npx multicorn-shield files stop --agent ${opts.agent}
@@ -5936,6 +5961,7 @@ Stop with: npx multicorn-shield files stop --agent ${opts.agent}
5936
5961
  if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
5937
5962
  if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
5938
5963
  if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
5964
+ if (opts.respawnProxy) args.push("--respawn-proxy");
5939
5965
  if (opts.client !== void 0) args.push("--client", opts.client);
5940
5966
  const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
5941
5967
  const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
@@ -6019,8 +6045,7 @@ async function runFilesCommand(opts) {
6019
6045
  const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
6020
6046
  const existingPidfile = readPidfile(opts.agent);
6021
6047
  if (existingPidfile !== null) {
6022
- const supervisorAlive = typeof existingPidfile.supervisorPid === "number" && isProcessAlive(existingPidfile.supervisorPid);
6023
- if (supervisorAlive) {
6048
+ if (isAgentSessionRunning(existingPidfile)) {
6024
6049
  process.stderr.write(
6025
6050
  `A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
6026
6051
  `
@@ -6034,7 +6059,9 @@ async function runFilesCommand(opts) {
6034
6059
  let fsReused;
6035
6060
  try {
6036
6061
  const ensured = await withResourceLock(async () => {
6037
- const proxyRes = await ensureProxy(proxyPort, config.baseUrl);
6062
+ const proxyRes = await ensureProxy(proxyPort, config.baseUrl, {
6063
+ forceRespawn: opts.respawnProxy
6064
+ });
6038
6065
  const fsRes = await ensureFsServer(realDir, opts.port);
6039
6066
  writePidfile({
6040
6067
  agent: opts.agent,
@@ -6330,6 +6357,8 @@ function parseArgs(argv) {
6330
6357
  let filesForeground = false;
6331
6358
  let filesStatus = false;
6332
6359
  let filesRestart = false;
6360
+ let filesRespawnProxy = false;
6361
+ let initIntegration = void 0;
6333
6362
  for (let i = 0; i < args.length; i++) {
6334
6363
  const arg = args[i];
6335
6364
  if (arg === "init") {
@@ -6380,6 +6409,8 @@ function parseArgs(argv) {
6380
6409
  filesStop = true;
6381
6410
  } else if (token === "--foreground") {
6382
6411
  filesForeground = true;
6412
+ } else if (token === "--respawn-proxy") {
6413
+ filesRespawnProxy = true;
6383
6414
  } else if (token === "--detach") ; else if (token === "stop") {
6384
6415
  filesStop = true;
6385
6416
  } else if (token === "status") {
@@ -6488,6 +6519,12 @@ function parseArgs(argv) {
6488
6519
  }
6489
6520
  } else if (arg === "--verbose" || arg === "--debug") {
6490
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
+ }
6491
6528
  }
6492
6529
  }
6493
6530
  return {
@@ -6501,6 +6538,7 @@ function parseArgs(argv) {
6501
6538
  deleteAgentName,
6502
6539
  apiKey,
6503
6540
  verbose,
6541
+ initIntegration,
6504
6542
  filesDir,
6505
6543
  filesPort,
6506
6544
  filesProxyPort,
@@ -6508,7 +6546,8 @@ function parseArgs(argv) {
6508
6546
  filesClient,
6509
6547
  filesForeground,
6510
6548
  filesStatus,
6511
- filesRestart
6549
+ filesRestart,
6550
+ filesRespawnProxy
6512
6551
  };
6513
6552
  }
6514
6553
  function printHelp() {
@@ -6518,7 +6557,12 @@ function printHelp() {
6518
6557
  "",
6519
6558
  "Usage:",
6520
6559
  " npx multicorn-shield init",
6521
- " Interactive setup. Saves API key to ~/.multicorn/config.json.",
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).",
6522
6566
  "",
6523
6567
  " npx multicorn-shield files <dir> --agent <name> [--client <client>]",
6524
6568
  " Share a local folder with a coding agent. Starts a filesystem MCP server",
@@ -6599,7 +6643,10 @@ async function runCli() {
6599
6643
  process.exit(0);
6600
6644
  }
6601
6645
  if (cli.subcommand === "init") {
6602
- await runInit(cli.baseUrl, { verbose: cli.verbose });
6646
+ await runInit(cli.baseUrl, {
6647
+ verbose: cli.verbose,
6648
+ ...cli.initIntegration !== void 0 ? { integration: cli.initIntegration } : {}
6649
+ });
6603
6650
  return;
6604
6651
  }
6605
6652
  if (cli.subcommand === "files") {
@@ -6615,7 +6662,8 @@ async function runCli() {
6615
6662
  client: void 0,
6616
6663
  foreground: true,
6617
6664
  status: true,
6618
- restart: false
6665
+ restart: false,
6666
+ respawnProxy: false
6619
6667
  });
6620
6668
  return;
6621
6669
  }
@@ -6640,7 +6688,8 @@ async function runCli() {
6640
6688
  client: cli.filesClient,
6641
6689
  foreground: cli.filesForeground,
6642
6690
  status: false,
6643
- restart: cli.filesRestart
6691
+ restart: cli.filesRestart,
6692
+ respawnProxy: cli.filesRespawnProxy
6644
6693
  });
6645
6694
  return;
6646
6695
  }