multicorn-shield 1.6.0 → 1.8.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 +30 -1
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/multicorn-proxy.js +527 -80
- package/dist/multicorn-shield.js +523 -79
- package/dist/shield-extension.js +8 -1
- package/package.json +4 -1
- package/plugins/opencode/multicorn-shield.ts +485 -0
package/dist/multicorn-proxy.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync, existsSync, statSync } from 'fs';
|
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import { createInterface } from 'readline';
|
|
11
|
+
import { parse, stringify } from 'yaml';
|
|
11
12
|
import 'stream';
|
|
12
13
|
|
|
13
14
|
var __defProp = Object.defineProperty;
|
|
@@ -924,6 +925,22 @@ require(${JSON.stringify(destPost)});
|
|
|
924
925
|
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
925
926
|
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
926
927
|
}
|
|
928
|
+
function getOpenCodeGlobalPluginsDir() {
|
|
929
|
+
return join(homedir(), ".config", "opencode", "plugins");
|
|
930
|
+
}
|
|
931
|
+
async function installOpenCodeNativePlugin() {
|
|
932
|
+
const root = multicornShieldPackageRoot();
|
|
933
|
+
const src = join(root, "plugins", "opencode", "multicorn-shield.ts");
|
|
934
|
+
if (!existsSync(src)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Could not find Shield OpenCode plugin at ${src}. If you use npm, install the latest multicorn-shield package.`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const destDir = getOpenCodeGlobalPluginsDir();
|
|
940
|
+
await mkdir(destDir, { recursive: true });
|
|
941
|
+
const dest = join(destDir, "multicorn-shield.ts");
|
|
942
|
+
await copyFile(src, dest);
|
|
943
|
+
}
|
|
927
944
|
async function promptClineIntegrationMode(ask) {
|
|
928
945
|
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
929
946
|
process.stderr.write(
|
|
@@ -1283,6 +1300,23 @@ async function promptGeminiCliIntegrationMode(ask) {
|
|
|
1283
1300
|
}
|
|
1284
1301
|
return choice === 1 ? "native" : "hosted";
|
|
1285
1302
|
}
|
|
1303
|
+
async function promptOpencodeIntegrationMode(ask) {
|
|
1304
|
+
process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
|
|
1305
|
+
process.stderr.write(
|
|
1306
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
|
|
1307
|
+
);
|
|
1308
|
+
process.stderr.write(
|
|
1309
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
|
|
1310
|
+
);
|
|
1311
|
+
let choice = 0;
|
|
1312
|
+
while (choice === 0) {
|
|
1313
|
+
const input = await ask("Choose integration (1-2): ");
|
|
1314
|
+
const num = parseInt(input.trim(), 10);
|
|
1315
|
+
if (num === 1) choice = 1;
|
|
1316
|
+
if (num === 2) choice = 2;
|
|
1317
|
+
}
|
|
1318
|
+
return choice === 1 ? "native" : "hosted";
|
|
1319
|
+
}
|
|
1286
1320
|
function getClaudeDesktopConfigPath() {
|
|
1287
1321
|
switch (process.platform) {
|
|
1288
1322
|
case "win32":
|
|
@@ -1346,9 +1380,6 @@ function getClineMcpSettingsPath() {
|
|
|
1346
1380
|
);
|
|
1347
1381
|
}
|
|
1348
1382
|
}
|
|
1349
|
-
function getContinueConfigJsonPath() {
|
|
1350
|
-
return join(homedir(), ".continue", "config.json");
|
|
1351
|
-
}
|
|
1352
1383
|
function platformMenuLabelForSelection(sel) {
|
|
1353
1384
|
const slug = PLATFORM_BY_SELECTION[sel];
|
|
1354
1385
|
if (slug === void 0) return "Unknown";
|
|
@@ -1650,13 +1681,56 @@ function writeMcpAddedLine(shortName, filePath) {
|
|
|
1650
1681
|
style.green("\u2713") + ' MCP server "' + shortName + '" added to ' + style.cyan(filePath) + "\n"
|
|
1651
1682
|
);
|
|
1652
1683
|
}
|
|
1653
|
-
|
|
1684
|
+
function sanitiseYamlValue(value) {
|
|
1685
|
+
if (value.length === 0) {
|
|
1686
|
+
return "''";
|
|
1687
|
+
}
|
|
1688
|
+
const needsQuoting = /[:#\n{}]/.test(value) || value.includes("[") || value.includes("]") || value !== value.trim() || value.includes("'");
|
|
1689
|
+
if (!needsQuoting) {
|
|
1690
|
+
return value;
|
|
1691
|
+
}
|
|
1692
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1693
|
+
}
|
|
1694
|
+
function gitignoreLikelyCoversPath(relPosixPath, gitignoreBody) {
|
|
1695
|
+
const norm = relPosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1696
|
+
const lines = gitignoreBody.split(/\r?\n/);
|
|
1697
|
+
for (const raw of lines) {
|
|
1698
|
+
const line = raw.trim();
|
|
1699
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
1700
|
+
const pat = line.replace(/^\//, "");
|
|
1701
|
+
if (pat === norm || pat === `./${norm}`) return true;
|
|
1702
|
+
if (!pat.includes("*")) {
|
|
1703
|
+
if (pat.endsWith("/")) {
|
|
1704
|
+
const dir = pat.slice(0, -1);
|
|
1705
|
+
if (norm === dir || norm.startsWith(`${dir}/`)) return true;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
|
|
1712
|
+
const gitignorePath = join(workspaceRoot, ".gitignore");
|
|
1713
|
+
let content;
|
|
1714
|
+
try {
|
|
1715
|
+
content = await readFile(gitignorePath, "utf8");
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
if (isErrnoException(e) && e.code === "ENOENT") return;
|
|
1718
|
+
throw e;
|
|
1719
|
+
}
|
|
1720
|
+
const norm = relativePosixPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1721
|
+
if (gitignoreLikelyCoversPath(norm, content)) return;
|
|
1722
|
+
process.stderr.write(
|
|
1723
|
+
style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
|
|
1654
1727
|
let root = {};
|
|
1655
1728
|
try {
|
|
1656
1729
|
const raw = await readFile(filePath, "utf8");
|
|
1730
|
+
const toParse = options.stripJsonComments ? raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "") : raw;
|
|
1657
1731
|
let parsed;
|
|
1658
1732
|
try {
|
|
1659
|
-
parsed = JSON.parse(
|
|
1733
|
+
parsed = JSON.parse(toParse);
|
|
1660
1734
|
} catch {
|
|
1661
1735
|
return "parse-error";
|
|
1662
1736
|
}
|
|
@@ -1672,15 +1746,25 @@ async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
|
|
|
1672
1746
|
throw e;
|
|
1673
1747
|
}
|
|
1674
1748
|
}
|
|
1675
|
-
const
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1749
|
+
const bucketRaw = root[topLevelKey];
|
|
1750
|
+
const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
|
|
1751
|
+
if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
|
|
1752
|
+
return "unchanged";
|
|
1753
|
+
}
|
|
1754
|
+
bucket[shortName] = entry;
|
|
1755
|
+
root[topLevelKey] = bucket;
|
|
1679
1756
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1680
1757
|
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
1681
1758
|
writeMcpAddedLine(shortName, filePath);
|
|
1682
1759
|
return "ok";
|
|
1683
1760
|
}
|
|
1761
|
+
async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
|
|
1762
|
+
const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
|
|
1763
|
+
stripJsonComments: false,
|
|
1764
|
+
onExisting: "overwrite"
|
|
1765
|
+
});
|
|
1766
|
+
return result === "parse-error" ? "parse-error" : "ok";
|
|
1767
|
+
}
|
|
1684
1768
|
async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
|
|
1685
1769
|
const entry = {
|
|
1686
1770
|
command: "npx",
|
|
@@ -1688,8 +1772,30 @@ async function mergeClaudeDesktopHostedMcpRemote(shortName, proxyUrl, apiKey) {
|
|
|
1688
1772
|
};
|
|
1689
1773
|
return mergeMcpServersObjectStyle(getClaudeDesktopConfigPath(), shortName, entry);
|
|
1690
1774
|
}
|
|
1691
|
-
async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
|
|
1692
|
-
const
|
|
1775
|
+
async function mergeContinueHostedMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
1776
|
+
const dir = join(workspacePath, ".continue", "mcpServers");
|
|
1777
|
+
const filePath = join(dir, `${shortName}.yaml`);
|
|
1778
|
+
const sn = sanitiseYamlValue(shortName);
|
|
1779
|
+
const urlEsc = sanitiseYamlValue(proxyUrl);
|
|
1780
|
+
const authEsc = sanitiseYamlValue(`Bearer ${apiKey}`);
|
|
1781
|
+
const yaml = `name: ${sn}
|
|
1782
|
+
version: 0.0.1
|
|
1783
|
+
schema: v1
|
|
1784
|
+
mcpServers:
|
|
1785
|
+
- name: ${sn}
|
|
1786
|
+
type: streamable-http
|
|
1787
|
+
url: ${urlEsc}
|
|
1788
|
+
headers:
|
|
1789
|
+
Authorization: ${authEsc}
|
|
1790
|
+
`;
|
|
1791
|
+
await mkdir(dir, { recursive: true });
|
|
1792
|
+
await writeFile(filePath, yaml, SECRET_JSON_FILE_OPTIONS);
|
|
1793
|
+
writeMcpAddedLine(shortName, filePath);
|
|
1794
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, `.continue/mcpServers/${shortName}.yaml`);
|
|
1795
|
+
return "ok";
|
|
1796
|
+
}
|
|
1797
|
+
async function mergeCopilotVscodeMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
1798
|
+
const filePath = join(workspacePath, ".vscode", "mcp.json");
|
|
1693
1799
|
let root = {};
|
|
1694
1800
|
try {
|
|
1695
1801
|
const raw = await readFile(filePath, "utf8");
|
|
@@ -1711,43 +1817,92 @@ async function mergeContinueHostedMcp(shortName, proxyUrl, apiKey) {
|
|
|
1711
1817
|
throw e;
|
|
1712
1818
|
}
|
|
1713
1819
|
}
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
return "parse-error";
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
const servers = Array.isArray(rawServers) ? rawServers.map((s) => ({ ...s })) : [];
|
|
1723
|
-
const entry = {
|
|
1724
|
-
name: shortName,
|
|
1725
|
-
type: "streamable-http",
|
|
1820
|
+
const serversRaw = root["servers"];
|
|
1821
|
+
const servers = typeof serversRaw === "object" && serversRaw !== null && !Array.isArray(serversRaw) ? { ...serversRaw } : {};
|
|
1822
|
+
const existed = servers[shortName] !== void 0;
|
|
1823
|
+
servers[shortName] = {
|
|
1824
|
+
type: "http",
|
|
1726
1825
|
url: proxyUrl,
|
|
1727
|
-
headers: {
|
|
1728
|
-
Authorization: `Bearer ${apiKey}`
|
|
1729
|
-
}
|
|
1826
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1730
1827
|
};
|
|
1731
|
-
|
|
1732
|
-
if (idx >= 0) {
|
|
1733
|
-
servers[idx] = entry;
|
|
1734
|
-
} else {
|
|
1735
|
-
servers.push(entry);
|
|
1736
|
-
}
|
|
1737
|
-
root["mcpServers"] = servers;
|
|
1828
|
+
root["servers"] = servers;
|
|
1738
1829
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1739
1830
|
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
1740
|
-
|
|
1831
|
+
if (existed) {
|
|
1832
|
+
process.stderr.write(
|
|
1833
|
+
style.dim(`Updated existing server entry for ${shortName} in .vscode/mcp.json`) + "\n"
|
|
1834
|
+
);
|
|
1835
|
+
} else {
|
|
1836
|
+
writeMcpAddedLine(shortName, filePath);
|
|
1837
|
+
}
|
|
1838
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, ".vscode/mcp.json");
|
|
1741
1839
|
return "ok";
|
|
1742
1840
|
}
|
|
1743
1841
|
async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
1744
|
-
const filePath = join(workspacePath, ".
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1842
|
+
const filePath = join(workspacePath, ".kilo", "kilo.jsonc");
|
|
1843
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
1844
|
+
filePath,
|
|
1845
|
+
"mcp",
|
|
1846
|
+
shortName,
|
|
1847
|
+
{
|
|
1848
|
+
type: "remote",
|
|
1849
|
+
url: proxyUrl,
|
|
1850
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1851
|
+
enabled: true
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
stripJsonComments: true,
|
|
1855
|
+
onExisting: "skip"
|
|
1749
1856
|
}
|
|
1750
|
-
|
|
1857
|
+
);
|
|
1858
|
+
if (result === "parse-error") return "parse-error";
|
|
1859
|
+
if (result === "ok") {
|
|
1860
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, ".kilo/kilo.jsonc");
|
|
1861
|
+
}
|
|
1862
|
+
return "ok";
|
|
1863
|
+
}
|
|
1864
|
+
async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
|
|
1865
|
+
try {
|
|
1866
|
+
const raw = await readFile(filePath, "utf8");
|
|
1867
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1868
|
+
let parsed;
|
|
1869
|
+
try {
|
|
1870
|
+
parsed = JSON.parse(stripped);
|
|
1871
|
+
} catch {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
1875
|
+
const root = parsed;
|
|
1876
|
+
const existingSchema = root["$schema"];
|
|
1877
|
+
if (typeof existingSchema === "string" && existingSchema.length > 0) return;
|
|
1878
|
+
root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
|
|
1879
|
+
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
1880
|
+
} catch {
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
1884
|
+
const filePath = join(workspacePath, "opencode.json");
|
|
1885
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
1886
|
+
filePath,
|
|
1887
|
+
"mcp",
|
|
1888
|
+
shortName,
|
|
1889
|
+
{
|
|
1890
|
+
type: "remote",
|
|
1891
|
+
url: proxyUrl,
|
|
1892
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1893
|
+
enabled: true
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
stripJsonComments: true,
|
|
1897
|
+
onExisting: "skip"
|
|
1898
|
+
}
|
|
1899
|
+
);
|
|
1900
|
+
if (result === "parse-error") return "parse-error";
|
|
1901
|
+
await injectOpencodeSchemaIntoConfigIfMissing(filePath);
|
|
1902
|
+
if (result === "ok") {
|
|
1903
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
|
|
1904
|
+
}
|
|
1905
|
+
return "ok";
|
|
1751
1906
|
}
|
|
1752
1907
|
function printHostedProxyJsonParseWarning(filePath) {
|
|
1753
1908
|
process.stderr.write(
|
|
@@ -1788,7 +1943,14 @@ function printHostedProxyPostWriteHints(platform, shortName) {
|
|
|
1788
1943
|
}
|
|
1789
1944
|
if (platform === "kilo-code") {
|
|
1790
1945
|
process.stderr.write(
|
|
1791
|
-
style.dim("Restart Kilo Code or reload the window so it picks up .
|
|
1946
|
+
style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
if (platform === "opencode") {
|
|
1950
|
+
process.stderr.write(
|
|
1951
|
+
style.dim(
|
|
1952
|
+
"Restart OpenCode or start a new session so it picks up opencode.json. For global MCP, merge the same snippet into ~/.config/opencode/opencode.json."
|
|
1953
|
+
) + "\n"
|
|
1792
1954
|
);
|
|
1793
1955
|
}
|
|
1794
1956
|
}
|
|
@@ -1805,15 +1967,40 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
1805
1967
|
return;
|
|
1806
1968
|
}
|
|
1807
1969
|
if (platform === "github-copilot") {
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1970
|
+
try {
|
|
1971
|
+
const result = await mergeCopilotVscodeMcp(
|
|
1972
|
+
workspacePath,
|
|
1973
|
+
shortName,
|
|
1974
|
+
proxyUrlWithKeyWhenNeeded,
|
|
1975
|
+
apiKey
|
|
1976
|
+
);
|
|
1977
|
+
if (result === "ok") {
|
|
1978
|
+
printHostedProxyPostWriteHints(platform, shortName);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
printHostedProxyJsonParseWarning(join(workspacePath, ".vscode", "mcp.json"));
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
process.stderr.write(
|
|
1984
|
+
`${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
|
|
1985
|
+
`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1813
1988
|
printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
|
|
1814
1989
|
return;
|
|
1815
1990
|
}
|
|
1816
1991
|
if (platform === "goose") {
|
|
1992
|
+
try {
|
|
1993
|
+
const result = await mergeGooseConfig(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
|
|
1994
|
+
if (result === "ok") {
|
|
1995
|
+
printHostedProxyPostWriteHints(platform, shortName);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
process.stderr.write(
|
|
2000
|
+
`${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
|
|
2001
|
+
`
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
1817
2004
|
printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
|
|
1818
2005
|
return;
|
|
1819
2006
|
}
|
|
@@ -1860,12 +2047,29 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
1860
2047
|
apiKey
|
|
1861
2048
|
);
|
|
1862
2049
|
if (result === "parse-error") {
|
|
1863
|
-
printHostedProxyJsonParseWarning(join(workspacePath, ".
|
|
2050
|
+
printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
|
|
2051
|
+
}
|
|
2052
|
+
} else if (platform === "opencode") {
|
|
2053
|
+
result = await mergeOpenCodeProjectMcp(
|
|
2054
|
+
workspacePath,
|
|
2055
|
+
shortName,
|
|
2056
|
+
proxyUrlWithKeyWhenNeeded,
|
|
2057
|
+
apiKey
|
|
2058
|
+
);
|
|
2059
|
+
if (result === "parse-error") {
|
|
2060
|
+
printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
|
|
1864
2061
|
}
|
|
1865
2062
|
} else if (platform === "continue-dev") {
|
|
1866
|
-
result = await mergeContinueHostedMcp(
|
|
2063
|
+
result = await mergeContinueHostedMcp(
|
|
2064
|
+
workspacePath,
|
|
2065
|
+
shortName,
|
|
2066
|
+
proxyUrlWithKeyWhenNeeded,
|
|
2067
|
+
apiKey
|
|
2068
|
+
);
|
|
1867
2069
|
if (result === "parse-error") {
|
|
1868
|
-
printHostedProxyJsonParseWarning(
|
|
2070
|
+
printHostedProxyJsonParseWarning(
|
|
2071
|
+
join(workspacePath, ".continue", "mcpServers", `${shortName}.yaml`)
|
|
2072
|
+
);
|
|
1869
2073
|
}
|
|
1870
2074
|
} else {
|
|
1871
2075
|
result = await mergeMcpServersObjectStyle(getCursorMcpJsonPath(), shortName, {
|
|
@@ -1889,17 +2093,96 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
1889
2093
|
}
|
|
1890
2094
|
printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
|
|
1891
2095
|
}
|
|
1892
|
-
function
|
|
1893
|
-
|
|
1894
|
-
|
|
2096
|
+
function printGooseConfigYamlParseErrorToStderr() {
|
|
2097
|
+
process.stderr.write(
|
|
2098
|
+
style.yellow("!") + " Could not parse ~/.config/goose/config.yaml - check for syntax errors or invalid YAML\n"
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
function gooseExtensionYaml(shortName, proxyUrl, bearerHeader) {
|
|
2102
|
+
const sn = sanitiseYamlValue(shortName);
|
|
2103
|
+
const urlEsc = sanitiseYamlValue(proxyUrl);
|
|
2104
|
+
const authEsc = sanitiseYamlValue(bearerHeader);
|
|
2105
|
+
return ` ${sn}:
|
|
2106
|
+
enabled: true
|
|
1895
2107
|
type: streamable_http
|
|
1896
|
-
|
|
2108
|
+
name: ${sn}
|
|
2109
|
+
description: ''
|
|
2110
|
+
uri: ${urlEsc}
|
|
2111
|
+
envs: {}
|
|
2112
|
+
env_keys: []
|
|
1897
2113
|
headers:
|
|
1898
|
-
Authorization: ${
|
|
1899
|
-
enabled: true
|
|
2114
|
+
Authorization: ${authEsc}
|
|
1900
2115
|
timeout: 300
|
|
2116
|
+
socket: null
|
|
2117
|
+
bundled: null
|
|
2118
|
+
available_tools: []
|
|
1901
2119
|
`;
|
|
1902
2120
|
}
|
|
2121
|
+
function gooseHostedProxyYaml(shortName, proxyUrl, bearerHeader) {
|
|
2122
|
+
return `extensions:
|
|
2123
|
+
` + gooseExtensionYaml(shortName, proxyUrl, bearerHeader);
|
|
2124
|
+
}
|
|
2125
|
+
function isYamlPlainObject(value) {
|
|
2126
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2127
|
+
}
|
|
2128
|
+
async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
|
|
2129
|
+
const filePath = join(homedir(), ".config", "goose", "config.yaml");
|
|
2130
|
+
const bearerHeader = `Bearer ${apiKey}`;
|
|
2131
|
+
let content = "";
|
|
2132
|
+
try {
|
|
2133
|
+
content = await readFile(filePath, "utf8");
|
|
2134
|
+
} catch (e) {
|
|
2135
|
+
if (isErrnoException(e) && e.code === "ENOENT") {
|
|
2136
|
+
content = "";
|
|
2137
|
+
} else {
|
|
2138
|
+
throw e;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
let root;
|
|
2142
|
+
try {
|
|
2143
|
+
const data = content.trim().length === 0 ? {} : parse(content);
|
|
2144
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
2145
|
+
printGooseConfigYamlParseErrorToStderr();
|
|
2146
|
+
return "parse-error";
|
|
2147
|
+
}
|
|
2148
|
+
root = data;
|
|
2149
|
+
} catch {
|
|
2150
|
+
printGooseConfigYamlParseErrorToStderr();
|
|
2151
|
+
return "parse-error";
|
|
2152
|
+
}
|
|
2153
|
+
const extensionsRaw = root["extensions"];
|
|
2154
|
+
let extensions;
|
|
2155
|
+
if (isYamlPlainObject(extensionsRaw)) {
|
|
2156
|
+
extensions = { ...extensionsRaw };
|
|
2157
|
+
} else if (extensionsRaw === void 0) {
|
|
2158
|
+
extensions = {};
|
|
2159
|
+
} else {
|
|
2160
|
+
printGooseConfigYamlParseErrorToStderr();
|
|
2161
|
+
return "parse-error";
|
|
2162
|
+
}
|
|
2163
|
+
extensions[shortName] = {
|
|
2164
|
+
enabled: true,
|
|
2165
|
+
type: "streamable_http",
|
|
2166
|
+
name: shortName,
|
|
2167
|
+
description: "",
|
|
2168
|
+
uri: proxyUrl,
|
|
2169
|
+
envs: {},
|
|
2170
|
+
env_keys: [],
|
|
2171
|
+
headers: { Authorization: bearerHeader },
|
|
2172
|
+
timeout: 300,
|
|
2173
|
+
socket: null,
|
|
2174
|
+
bundled: null,
|
|
2175
|
+
available_tools: []
|
|
2176
|
+
};
|
|
2177
|
+
root["extensions"] = extensions;
|
|
2178
|
+
const out = stringify(root, { indent: 2, lineWidth: 0 });
|
|
2179
|
+
const body = out.endsWith("\n") ? out : `${out}
|
|
2180
|
+
`;
|
|
2181
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
2182
|
+
await writeFile(filePath, body, SECRET_JSON_FILE_OPTIONS);
|
|
2183
|
+
writeMcpAddedLine(shortName, filePath);
|
|
2184
|
+
return "ok";
|
|
2185
|
+
}
|
|
1903
2186
|
function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
1904
2187
|
const hostedInlinePlatforms = /* @__PURE__ */ new Set([
|
|
1905
2188
|
"cursor",
|
|
@@ -1910,7 +2193,8 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
1910
2193
|
"kilo-code",
|
|
1911
2194
|
"github-copilot",
|
|
1912
2195
|
"continue-dev",
|
|
1913
|
-
"goose"
|
|
2196
|
+
"goose",
|
|
2197
|
+
"opencode"
|
|
1914
2198
|
]);
|
|
1915
2199
|
const usesInlineKey = hostedInlinePlatforms.has(platform);
|
|
1916
2200
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
@@ -1919,14 +2203,12 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
1919
2203
|
if (platform === "github-copilot") {
|
|
1920
2204
|
snippetText = JSON.stringify(
|
|
1921
2205
|
{
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
Authorization: authHeader
|
|
1929
|
-
}
|
|
2206
|
+
servers: {
|
|
2207
|
+
[shortName]: {
|
|
2208
|
+
type: "http",
|
|
2209
|
+
url: urlInSnippet,
|
|
2210
|
+
headers: {
|
|
2211
|
+
Authorization: authHeader
|
|
1930
2212
|
}
|
|
1931
2213
|
}
|
|
1932
2214
|
}
|
|
@@ -1965,18 +2247,50 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
1965
2247
|
2
|
|
1966
2248
|
);
|
|
1967
2249
|
} else if (platform === "continue-dev") {
|
|
2250
|
+
const sn = sanitiseYamlValue(shortName);
|
|
2251
|
+
const urlEsc = sanitiseYamlValue(urlInSnippet);
|
|
2252
|
+
const authEsc = sanitiseYamlValue(authHeader);
|
|
2253
|
+
snippetText = `name: ${sn}
|
|
2254
|
+
version: 0.0.1
|
|
2255
|
+
schema: v1
|
|
2256
|
+
mcpServers:
|
|
2257
|
+
- name: ${sn}
|
|
2258
|
+
type: streamable-http
|
|
2259
|
+
url: ${urlEsc}
|
|
2260
|
+
headers:
|
|
2261
|
+
Authorization: ${authEsc}
|
|
2262
|
+
`;
|
|
2263
|
+
} else if (platform === "kilo-code") {
|
|
1968
2264
|
snippetText = JSON.stringify(
|
|
1969
2265
|
{
|
|
1970
|
-
|
|
1971
|
-
{
|
|
1972
|
-
|
|
1973
|
-
type: "streamable-http",
|
|
2266
|
+
mcp: {
|
|
2267
|
+
[shortName]: {
|
|
2268
|
+
type: "remote",
|
|
1974
2269
|
url: urlInSnippet,
|
|
1975
2270
|
headers: {
|
|
1976
2271
|
Authorization: authHeader
|
|
1977
|
-
}
|
|
2272
|
+
},
|
|
2273
|
+
enabled: true
|
|
1978
2274
|
}
|
|
1979
|
-
|
|
2275
|
+
}
|
|
2276
|
+
},
|
|
2277
|
+
null,
|
|
2278
|
+
2
|
|
2279
|
+
);
|
|
2280
|
+
} else if (platform === "opencode") {
|
|
2281
|
+
snippetText = JSON.stringify(
|
|
2282
|
+
{
|
|
2283
|
+
$schema: OPENCODE_CONFIG_SCHEMA_URL,
|
|
2284
|
+
mcp: {
|
|
2285
|
+
[shortName]: {
|
|
2286
|
+
type: "remote",
|
|
2287
|
+
url: urlInSnippet,
|
|
2288
|
+
headers: {
|
|
2289
|
+
Authorization: authHeader
|
|
2290
|
+
},
|
|
2291
|
+
enabled: true
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
1980
2294
|
},
|
|
1981
2295
|
null,
|
|
1982
2296
|
2
|
|
@@ -2016,16 +2330,24 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
2016
2330
|
);
|
|
2017
2331
|
} else if (platform === "kilo-code") {
|
|
2018
2332
|
process.stderr.write(
|
|
2019
|
-
"\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".
|
|
2333
|
+
"\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
|
|
2334
|
+
);
|
|
2335
|
+
} else if (platform === "opencode") {
|
|
2336
|
+
process.stderr.write(
|
|
2337
|
+
"\n" + style.dim(
|
|
2338
|
+
"Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for global config). OpenCode detects configured MCP servers on the next session start."
|
|
2339
|
+
) + "\n\n"
|
|
2020
2340
|
);
|
|
2021
2341
|
} else if (platform === "github-copilot") {
|
|
2022
2342
|
process.stderr.write(
|
|
2023
2343
|
"\n" + style.dim(
|
|
2024
|
-
"
|
|
2344
|
+
"Create .vscode/mcp.json in your workspace root (create the .vscode folder if it does not exist). After saving, reload VS Code and confirm the server appears in Copilot Agent mode under Tools."
|
|
2025
2345
|
) + "\n\n"
|
|
2026
2346
|
);
|
|
2027
2347
|
} else if (platform === "continue-dev") {
|
|
2028
|
-
process.stderr.write(
|
|
2348
|
+
process.stderr.write(
|
|
2349
|
+
"\n" + style.dim(`Save this as .continue/mcpServers/${shortName}.yaml in your workspace root.`) + "\n\n"
|
|
2350
|
+
);
|
|
2029
2351
|
} else if (platform === "goose") {
|
|
2030
2352
|
process.stderr.write(
|
|
2031
2353
|
"\n" + style.dim("Add this to ~/.config/goose/config.yaml under the extensions key.") + "\n\n"
|
|
@@ -2079,6 +2401,11 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
2079
2401
|
if (platform === "goose") {
|
|
2080
2402
|
process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
|
|
2081
2403
|
}
|
|
2404
|
+
if (platform === "opencode") {
|
|
2405
|
+
process.stderr.write(
|
|
2406
|
+
style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2082
2409
|
}
|
|
2083
2410
|
function agentDisplayNameDedupeKey(name) {
|
|
2084
2411
|
return name.trim().normalize("NFKC").toLowerCase();
|
|
@@ -2201,7 +2528,7 @@ async function runInit(explicitBaseUrl, options) {
|
|
|
2201
2528
|
}
|
|
2202
2529
|
await warnIfInstalledShieldIsOutdated();
|
|
2203
2530
|
const configuredAgents = [];
|
|
2204
|
-
let currentAgents = collectAgentsFromConfig(existing);
|
|
2531
|
+
let currentAgents = mergeAgentsForUniqueNames(collectAgentsFromConfig(existing));
|
|
2205
2532
|
let lastConfig = {
|
|
2206
2533
|
apiKey,
|
|
2207
2534
|
baseUrl: resolvedBaseUrl,
|
|
@@ -2638,6 +2965,95 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2638
2965
|
setupSucceeded = true;
|
|
2639
2966
|
}
|
|
2640
2967
|
}
|
|
2968
|
+
} else if (selectedPlatform === "opencode") {
|
|
2969
|
+
const opencodeMode = await promptOpencodeIntegrationMode(ask);
|
|
2970
|
+
if (opencodeMode === "native") {
|
|
2971
|
+
try {
|
|
2972
|
+
await installOpenCodeNativePlugin();
|
|
2973
|
+
process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
|
|
2974
|
+
process.stderr.write(
|
|
2975
|
+
style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
|
|
2976
|
+
);
|
|
2977
|
+
process.stderr.write("\n");
|
|
2978
|
+
process.stderr.write(
|
|
2979
|
+
style.dim(
|
|
2980
|
+
"Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
|
|
2981
|
+
) + "\n"
|
|
2982
|
+
);
|
|
2983
|
+
process.stderr.write("\n");
|
|
2984
|
+
process.stderr.write(
|
|
2985
|
+
style.dim(
|
|
2986
|
+
"Note: Tool calls delegated to subagents via the task tool are not intercepted by this plugin (OpenCode limitation). Use the hosted proxy path for MCP traffic you route through Shield for broader coverage."
|
|
2987
|
+
) + "\n"
|
|
2988
|
+
);
|
|
2989
|
+
configuredAgents.push({
|
|
2990
|
+
selection,
|
|
2991
|
+
platform: selectedPlatform,
|
|
2992
|
+
platformLabel: selectedLabel,
|
|
2993
|
+
agentName,
|
|
2994
|
+
opencodeCliIntegration: "native"
|
|
2995
|
+
});
|
|
2996
|
+
setupSucceeded = true;
|
|
2997
|
+
} catch (error) {
|
|
2998
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2999
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
3000
|
+
}
|
|
3001
|
+
} else {
|
|
3002
|
+
const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
|
|
3003
|
+
let proxyUrl = "";
|
|
3004
|
+
let created = false;
|
|
3005
|
+
while (!created) {
|
|
3006
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
3007
|
+
try {
|
|
3008
|
+
proxyUrl = await createProxyConfig(
|
|
3009
|
+
resolvedBaseUrl,
|
|
3010
|
+
apiKey,
|
|
3011
|
+
agentName,
|
|
3012
|
+
targetUrl,
|
|
3013
|
+
shortName,
|
|
3014
|
+
selectedPlatform,
|
|
3015
|
+
upstreamHeaders
|
|
3016
|
+
);
|
|
3017
|
+
spinner.stop(true, "Proxy config created!");
|
|
3018
|
+
created = true;
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3021
|
+
spinner.stop(false, detail);
|
|
3022
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
3023
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
if (created && proxyUrl.length > 0) {
|
|
3029
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
3030
|
+
process.stderr.write(
|
|
3031
|
+
" " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
|
|
3032
|
+
);
|
|
3033
|
+
await applyHostedProxyMcpConfig(
|
|
3034
|
+
selectedPlatform,
|
|
3035
|
+
proxyUrl,
|
|
3036
|
+
shortName,
|
|
3037
|
+
apiKey,
|
|
3038
|
+
initWorkspacePath
|
|
3039
|
+
);
|
|
3040
|
+
process.stderr.write(
|
|
3041
|
+
"\n" + style.dim(
|
|
3042
|
+
"Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for global config). OpenCode detects configured MCP servers on the next session start."
|
|
3043
|
+
) + "\n"
|
|
3044
|
+
);
|
|
3045
|
+
configuredAgents.push({
|
|
3046
|
+
selection,
|
|
3047
|
+
platform: selectedPlatform,
|
|
3048
|
+
platformLabel: selectedLabel,
|
|
3049
|
+
agentName,
|
|
3050
|
+
shortName,
|
|
3051
|
+
proxyUrl,
|
|
3052
|
+
opencodeCliIntegration: "hosted"
|
|
3053
|
+
});
|
|
3054
|
+
setupSucceeded = true;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
2641
3057
|
} else if (selectedPlatform === "cline") {
|
|
2642
3058
|
const clineMode = await promptClineIntegrationMode(ask);
|
|
2643
3059
|
if (clineMode === "native") {
|
|
@@ -2862,23 +3278,27 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2862
3278
|
);
|
|
2863
3279
|
}
|
|
2864
3280
|
if (configuredPlatforms.has("kilo-code")) {
|
|
3281
|
+
const kiloLabel = mcpPromptLabel2("kilo-code");
|
|
2865
3282
|
blocks.push(
|
|
2866
|
-
"\n" + style.bold("Kilo Code") +
|
|
3283
|
+
"\n" + style.bold("Kilo Code") + '\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Confirm connection: Settings \u2192 Agent Behaviour \u2192 MCP Servers\n \u2192 Try it: paste this into Kilo Code:\n "Use the ' + kiloLabel + ' MCP server to list my GitHub repositories"\n'
|
|
2867
3284
|
);
|
|
2868
3285
|
}
|
|
2869
3286
|
if (configuredPlatforms.has("github-copilot")) {
|
|
3287
|
+
const copilotLabel = mcpPromptLabel2("github-copilot");
|
|
2870
3288
|
blocks.push(
|
|
2871
|
-
"\n" + style.bold("GitHub Copilot") +
|
|
3289
|
+
"\n" + style.bold("GitHub Copilot") + '\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Confirm connection: open Copilot chat in Agent mode and confirm the server appears under Tools\n \u2192 Try it: paste this into GitHub Copilot:\n "Use the ' + copilotLabel + ' MCP server to list my GitHub repositories"\n'
|
|
2872
3290
|
);
|
|
2873
3291
|
}
|
|
2874
3292
|
if (configuredPlatforms.has("continue-dev")) {
|
|
3293
|
+
const continueLabel = mcpPromptLabel2("continue-dev");
|
|
2875
3294
|
blocks.push(
|
|
2876
|
-
"\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") +
|
|
3295
|
+
"\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + '\n \u2192 Reload VS Code and open Continue agent mode\n \u2192 Confirm connection: Settings \u2192 Tools \u2192 MCP Servers\n \u2192 Try it: paste this into Continue:\n "Use the ' + continueLabel + ' MCP server to list my GitHub repositories"\n'
|
|
2877
3296
|
);
|
|
2878
3297
|
}
|
|
2879
3298
|
if (configuredPlatforms.has("goose")) {
|
|
3299
|
+
const gooseLabel = mcpPromptLabel2("goose");
|
|
2880
3300
|
blocks.push(
|
|
2881
|
-
"\n" + style.bold("Goose") +
|
|
3301
|
+
"\n" + style.bold("Goose") + '\n \u2192 Start a new Goose session after updating config\n \u2192 Confirm connection: check the Extensions page in the sidebar\n \u2192 Try it: paste this into Goose:\n "Use the ' + gooseLabel + ' MCP server to list my GitHub repositories"\n'
|
|
2882
3302
|
);
|
|
2883
3303
|
}
|
|
2884
3304
|
const windsurfNativeConfigured = configuredAgents.some(
|
|
@@ -2929,6 +3349,23 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2929
3349
|
"\n" + style.bold("Gemini CLI (hosted)") + "\n \u2192 Try it: make a request in Gemini CLI - Shield will intercept the first tool call and ask for your consent\n"
|
|
2930
3350
|
);
|
|
2931
3351
|
}
|
|
3352
|
+
const opencodeNativeConfigured = configuredAgents.some(
|
|
3353
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
|
|
3354
|
+
);
|
|
3355
|
+
const opencodeHostedConfigured = configuredAgents.some(
|
|
3356
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
|
|
3357
|
+
);
|
|
3358
|
+
if (opencodeNativeConfigured) {
|
|
3359
|
+
blocks.push(
|
|
3360
|
+
"\n" + style.bold("OpenCode (native)") + "\n \u2192 If you just installed OpenCode, open a new terminal tab (or run: source ~/.zshrc)\n \u2192 Restart OpenCode so it loads ~/.config/opencode/plugins/multicorn-shield.ts\n \u2192 Try it: trigger a primary-agent tool call - Shield will intercept the first actionable tool and ask for your consent\n"
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
if (opencodeHostedConfigured) {
|
|
3364
|
+
const ocLabel = mcpPromptLabel2("opencode");
|
|
3365
|
+
blocks.push(
|
|
3366
|
+
"\n" + style.bold("OpenCode (hosted)") + '\n \u2192 Restart OpenCode or start a new session after saving opencode.json\n \u2192 Try it: paste this into OpenCode:\n "Use the ' + ocLabel + ' MCP server to list allowed directories"\n'
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
2932
3369
|
if (configuredPlatforms.has("other-mcp")) {
|
|
2933
3370
|
blocks.push(
|
|
2934
3371
|
"\n" + style.bold("Local MCP / Other") + "\n \u2192 Run your configured wrap command (for example " + style.cyan("npx multicorn-shield --wrap ...") + ")\n \u2192 Try it: make a request in your coding agent - Shield will intercept the first tool call and ask for your consent\n"
|
|
@@ -2948,7 +3385,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2948
3385
|
}
|
|
2949
3386
|
return lastConfig;
|
|
2950
3387
|
}
|
|
2951
|
-
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, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, DEFAULT_SHIELD_API_BASE_URL;
|
|
3388
|
+
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, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL;
|
|
2952
3389
|
var init_config = __esm({
|
|
2953
3390
|
"src/proxy/config.ts"() {
|
|
2954
3391
|
init_consent();
|
|
@@ -2988,6 +3425,12 @@ var init_config = __esm({
|
|
|
2988
3425
|
{ slug: "windsurf", displayName: "Windsurf", section: "native" },
|
|
2989
3426
|
{ slug: "cline", displayName: "Cline", section: "native" },
|
|
2990
3427
|
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
|
|
3428
|
+
{
|
|
3429
|
+
slug: "opencode",
|
|
3430
|
+
displayName: "OpenCode",
|
|
3431
|
+
section: "native",
|
|
3432
|
+
prereqUrl: "https://opencode.ai"
|
|
3433
|
+
},
|
|
2991
3434
|
{
|
|
2992
3435
|
slug: "cursor",
|
|
2993
3436
|
displayName: "Cursor",
|
|
@@ -3048,6 +3491,7 @@ var init_config = __esm({
|
|
|
3048
3491
|
"continue-dev",
|
|
3049
3492
|
"goose"
|
|
3050
3493
|
]);
|
|
3494
|
+
OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
3051
3495
|
DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
|
|
3052
3496
|
}
|
|
3053
3497
|
});
|
|
@@ -3992,7 +4436,7 @@ var init_package = __esm({
|
|
|
3992
4436
|
"package.json"() {
|
|
3993
4437
|
package_default = {
|
|
3994
4438
|
name: "multicorn-shield",
|
|
3995
|
-
version: "1.
|
|
4439
|
+
version: "1.8.0",
|
|
3996
4440
|
description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
3997
4441
|
license: "MIT",
|
|
3998
4442
|
author: "Multicorn AI Pty Ltd",
|
|
@@ -4032,6 +4476,7 @@ var init_package = __esm({
|
|
|
4032
4476
|
"plugins/windsurf",
|
|
4033
4477
|
"plugins/cline",
|
|
4034
4478
|
"plugins/gemini-cli",
|
|
4479
|
+
"plugins/opencode",
|
|
4035
4480
|
"LICENSE",
|
|
4036
4481
|
"README.md",
|
|
4037
4482
|
"CHANGELOG.md"
|
|
@@ -4083,12 +4528,14 @@ var init_package = __esm({
|
|
|
4083
4528
|
dependencies: {
|
|
4084
4529
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
4085
4530
|
lit: "^3.2.0",
|
|
4531
|
+
yaml: "^2.8.2",
|
|
4086
4532
|
zod: "^4.3.6"
|
|
4087
4533
|
},
|
|
4088
4534
|
devDependencies: {
|
|
4089
4535
|
"@anthropic-ai/mcpb": "^2.1.2",
|
|
4090
4536
|
"@eslint/js": "^9.19.0",
|
|
4091
4537
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
4538
|
+
"@opencode-ai/plugin": "^1.14.48",
|
|
4092
4539
|
"@size-limit/file": "^11.1.6",
|
|
4093
4540
|
"@types/node": "^22.0.0",
|
|
4094
4541
|
"@vitest/coverage-v8": "^3.0.5",
|