multicorn-shield 1.7.0 → 1.9.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 +39 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/multicorn-proxy.js +465 -3
- package/dist/multicorn-shield.js +462 -2
- package/dist/shield-extension.js +13 -1
- package/package.json +3 -1
- package/plugins/opencode/multicorn-shield.ts +485 -0
package/dist/multicorn-shield.js
CHANGED
|
@@ -939,6 +939,115 @@ require(${JSON.stringify(destPost)});
|
|
|
939
939
|
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
940
940
|
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
941
941
|
}
|
|
942
|
+
function getOpenCodeGlobalPluginsDir() {
|
|
943
|
+
return join(homedir(), ".config", "opencode", "plugins");
|
|
944
|
+
}
|
|
945
|
+
async function installOpenCodeNativePlugin() {
|
|
946
|
+
const root = multicornShieldPackageRoot();
|
|
947
|
+
const src = join(root, "plugins", "opencode", "multicorn-shield.ts");
|
|
948
|
+
if (!existsSync(src)) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`Could not find Shield OpenCode plugin at ${src}. If you use npm, install the latest multicorn-shield package.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const destDir = getOpenCodeGlobalPluginsDir();
|
|
954
|
+
await mkdir(destDir, { recursive: true });
|
|
955
|
+
const dest = join(destDir, "multicorn-shield.ts");
|
|
956
|
+
await copyFile(src, dest);
|
|
957
|
+
}
|
|
958
|
+
function getCodexCliHooksInstallDir() {
|
|
959
|
+
return join(homedir(), ".multicorn", "codex-cli-hooks");
|
|
960
|
+
}
|
|
961
|
+
function getCodexConfigTomlPath() {
|
|
962
|
+
return join(homedir(), ".codex", "config.toml");
|
|
963
|
+
}
|
|
964
|
+
function getCodexHooksJsonPath() {
|
|
965
|
+
return join(homedir(), ".codex", "hooks.json");
|
|
966
|
+
}
|
|
967
|
+
async function installCodexCliNativeHooks() {
|
|
968
|
+
const root = multicornShieldPackageRoot();
|
|
969
|
+
const scriptsDir = join(root, "plugins", "codex-cli", "hooks", "scripts");
|
|
970
|
+
const preHook = join(scriptsDir, "pre-tool-use.cjs");
|
|
971
|
+
const postHook = join(scriptsDir, "post-tool-use.cjs");
|
|
972
|
+
const toolMap = join(scriptsDir, "codex-cli-tool-map.cjs");
|
|
973
|
+
const sharedHook = join(scriptsDir, "codex-cli-hooks-shared.cjs");
|
|
974
|
+
if (!existsSync(preHook) || !existsSync(postHook) || !existsSync(toolMap) || !existsSync(sharedHook)) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`Could not find Shield Codex CLI hook scripts at ${scriptsDir}. If you use npm, install the latest multicorn-shield package.`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
const destDir = getCodexCliHooksInstallDir();
|
|
980
|
+
await mkdir(destDir, { recursive: true });
|
|
981
|
+
await copyFile(preHook, join(destDir, "pre-tool-use.cjs"));
|
|
982
|
+
await copyFile(postHook, join(destDir, "post-tool-use.cjs"));
|
|
983
|
+
await copyFile(toolMap, join(destDir, "codex-cli-tool-map.cjs"));
|
|
984
|
+
await copyFile(sharedHook, join(destDir, "codex-cli-hooks-shared.cjs"));
|
|
985
|
+
const configTomlPath = getCodexConfigTomlPath();
|
|
986
|
+
await mkdir(join(homedir(), ".codex"), { recursive: true });
|
|
987
|
+
let tomlContent = "";
|
|
988
|
+
try {
|
|
989
|
+
tomlContent = await readFile(configTomlPath, "utf8");
|
|
990
|
+
} catch (err) {
|
|
991
|
+
if (!isErrnoException(err) || err.code !== "ENOENT") {
|
|
992
|
+
throw err;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (tomlContent.includes("codex_hooks")) {
|
|
996
|
+
tomlContent = tomlContent.replace(/codex_hooks/g, "hooks");
|
|
997
|
+
await writeFile(configTomlPath, tomlContent, "utf8");
|
|
998
|
+
}
|
|
999
|
+
if (!/\bhooks\s*=\s*true/.test(tomlContent)) {
|
|
1000
|
+
tomlContent = tomlContent.includes("[features]") ? tomlContent.replace("[features]", "[features]\nhooks = true") : tomlContent.trimEnd() + (tomlContent.length > 0 ? "\n\n" : "") + "[features]\nhooks = true\n";
|
|
1001
|
+
await writeFile(configTomlPath, tomlContent, "utf8");
|
|
1002
|
+
}
|
|
1003
|
+
const hooksConfig = {
|
|
1004
|
+
hooks: {
|
|
1005
|
+
PreToolUse: [
|
|
1006
|
+
{
|
|
1007
|
+
matcher: "",
|
|
1008
|
+
hooks: [
|
|
1009
|
+
{
|
|
1010
|
+
type: "command",
|
|
1011
|
+
command: `node ${join(destDir, "pre-tool-use.cjs")}`,
|
|
1012
|
+
statusMessage: "Checking Shield permissions"
|
|
1013
|
+
}
|
|
1014
|
+
]
|
|
1015
|
+
}
|
|
1016
|
+
],
|
|
1017
|
+
PostToolUse: [
|
|
1018
|
+
{
|
|
1019
|
+
matcher: "",
|
|
1020
|
+
hooks: [
|
|
1021
|
+
{
|
|
1022
|
+
type: "command",
|
|
1023
|
+
command: `node ${join(destDir, "post-tool-use.cjs")}`,
|
|
1024
|
+
timeout: 30,
|
|
1025
|
+
statusMessage: "Logging to Shield"
|
|
1026
|
+
}
|
|
1027
|
+
]
|
|
1028
|
+
}
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
|
|
1033
|
+
}
|
|
1034
|
+
async function promptCodexCliIntegrationMode(ask) {
|
|
1035
|
+
process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
|
|
1036
|
+
process.stderr.write(
|
|
1037
|
+
" " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
|
|
1038
|
+
);
|
|
1039
|
+
process.stderr.write(
|
|
1040
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
|
|
1041
|
+
);
|
|
1042
|
+
let choice = 0;
|
|
1043
|
+
while (choice === 0) {
|
|
1044
|
+
const input = await ask("Choose integration (1-2): ");
|
|
1045
|
+
const num = parseInt(input.trim(), 10);
|
|
1046
|
+
if (num === 1) choice = 1;
|
|
1047
|
+
if (num === 2) choice = 2;
|
|
1048
|
+
}
|
|
1049
|
+
return choice === 1 ? "native" : "hosted";
|
|
1050
|
+
}
|
|
942
1051
|
async function promptClineIntegrationMode(ask) {
|
|
943
1052
|
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
944
1053
|
process.stderr.write(
|
|
@@ -1298,6 +1407,23 @@ async function promptGeminiCliIntegrationMode(ask) {
|
|
|
1298
1407
|
}
|
|
1299
1408
|
return choice === 1 ? "native" : "hosted";
|
|
1300
1409
|
}
|
|
1410
|
+
async function promptOpencodeIntegrationMode(ask) {
|
|
1411
|
+
process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
|
|
1412
|
+
process.stderr.write(
|
|
1413
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
|
|
1414
|
+
);
|
|
1415
|
+
process.stderr.write(
|
|
1416
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
|
|
1417
|
+
);
|
|
1418
|
+
let choice = 0;
|
|
1419
|
+
while (choice === 0) {
|
|
1420
|
+
const input = await ask("Choose integration (1-2): ");
|
|
1421
|
+
const num = parseInt(input.trim(), 10);
|
|
1422
|
+
if (num === 1) choice = 1;
|
|
1423
|
+
if (num === 2) choice = 2;
|
|
1424
|
+
}
|
|
1425
|
+
return choice === 1 ? "native" : "hosted";
|
|
1426
|
+
}
|
|
1301
1427
|
function getClaudeDesktopConfigPath() {
|
|
1302
1428
|
switch (process.platform) {
|
|
1303
1429
|
case "win32":
|
|
@@ -1367,6 +1493,18 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
|
1367
1493
|
{ slug: "windsurf", displayName: "Windsurf", section: "native" },
|
|
1368
1494
|
{ slug: "cline", displayName: "Cline", section: "native" },
|
|
1369
1495
|
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
|
|
1496
|
+
{
|
|
1497
|
+
slug: "opencode",
|
|
1498
|
+
displayName: "OpenCode",
|
|
1499
|
+
section: "native",
|
|
1500
|
+
prereqUrl: "https://opencode.ai"
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
slug: "codex-cli",
|
|
1504
|
+
displayName: "Codex CLI",
|
|
1505
|
+
section: "native",
|
|
1506
|
+
prereqUrl: "https://github.com/openai/codex"
|
|
1507
|
+
},
|
|
1370
1508
|
{
|
|
1371
1509
|
slug: "cursor",
|
|
1372
1510
|
displayName: "Cursor",
|
|
@@ -1908,6 +2046,50 @@ async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKe
|
|
|
1908
2046
|
}
|
|
1909
2047
|
return "ok";
|
|
1910
2048
|
}
|
|
2049
|
+
var OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
2050
|
+
async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
|
|
2051
|
+
try {
|
|
2052
|
+
const raw = await readFile(filePath, "utf8");
|
|
2053
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2054
|
+
let parsed;
|
|
2055
|
+
try {
|
|
2056
|
+
parsed = JSON.parse(stripped);
|
|
2057
|
+
} catch {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
2061
|
+
const root = parsed;
|
|
2062
|
+
const existingSchema = root["$schema"];
|
|
2063
|
+
if (typeof existingSchema === "string" && existingSchema.length > 0) return;
|
|
2064
|
+
root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
|
|
2065
|
+
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
2070
|
+
const filePath = join(workspacePath, "opencode.json");
|
|
2071
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
2072
|
+
filePath,
|
|
2073
|
+
"mcp",
|
|
2074
|
+
shortName,
|
|
2075
|
+
{
|
|
2076
|
+
type: "remote",
|
|
2077
|
+
url: proxyUrl,
|
|
2078
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
2079
|
+
enabled: true
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
stripJsonComments: true,
|
|
2083
|
+
onExisting: "skip"
|
|
2084
|
+
}
|
|
2085
|
+
);
|
|
2086
|
+
if (result === "parse-error") return "parse-error";
|
|
2087
|
+
await injectOpencodeSchemaIntoConfigIfMissing(filePath);
|
|
2088
|
+
if (result === "ok") {
|
|
2089
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
|
|
2090
|
+
}
|
|
2091
|
+
return "ok";
|
|
2092
|
+
}
|
|
1911
2093
|
function printHostedProxyJsonParseWarning(filePath) {
|
|
1912
2094
|
process.stderr.write(
|
|
1913
2095
|
style.yellow("\u26A0") + " Could not parse JSON at " + style.cyan(filePath) + style.dim(" - showing paste snippet instead.") + "\n"
|
|
@@ -1950,6 +2132,13 @@ function printHostedProxyPostWriteHints(platform, shortName) {
|
|
|
1950
2132
|
style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
|
|
1951
2133
|
);
|
|
1952
2134
|
}
|
|
2135
|
+
if (platform === "opencode") {
|
|
2136
|
+
process.stderr.write(
|
|
2137
|
+
style.dim(
|
|
2138
|
+
"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."
|
|
2139
|
+
) + "\n"
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
1953
2142
|
}
|
|
1954
2143
|
async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey, workspacePath) {
|
|
1955
2144
|
const authHeader = `Bearer ${apiKey}`;
|
|
@@ -2046,6 +2235,19 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
2046
2235
|
if (result === "parse-error") {
|
|
2047
2236
|
printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
|
|
2048
2237
|
}
|
|
2238
|
+
} else if (platform === "opencode") {
|
|
2239
|
+
result = await mergeOpenCodeProjectMcp(
|
|
2240
|
+
workspacePath,
|
|
2241
|
+
shortName,
|
|
2242
|
+
proxyUrlWithKeyWhenNeeded,
|
|
2243
|
+
apiKey
|
|
2244
|
+
);
|
|
2245
|
+
if (result === "parse-error") {
|
|
2246
|
+
printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
|
|
2247
|
+
}
|
|
2248
|
+
} else if (platform === "codex-cli") {
|
|
2249
|
+
printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
|
|
2250
|
+
return;
|
|
2049
2251
|
} else if (platform === "continue-dev") {
|
|
2050
2252
|
result = await mergeContinueHostedMcp(
|
|
2051
2253
|
workspacePath,
|
|
@@ -2180,7 +2382,9 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
2180
2382
|
"kilo-code",
|
|
2181
2383
|
"github-copilot",
|
|
2182
2384
|
"continue-dev",
|
|
2183
|
-
"goose"
|
|
2385
|
+
"goose",
|
|
2386
|
+
"opencode",
|
|
2387
|
+
"codex-cli"
|
|
2184
2388
|
]);
|
|
2185
2389
|
const usesInlineKey = hostedInlinePlatforms.has(platform);
|
|
2186
2390
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
@@ -2263,6 +2467,29 @@ mcpServers:
|
|
|
2263
2467
|
null,
|
|
2264
2468
|
2
|
|
2265
2469
|
);
|
|
2470
|
+
} else if (platform === "opencode") {
|
|
2471
|
+
snippetText = JSON.stringify(
|
|
2472
|
+
{
|
|
2473
|
+
$schema: OPENCODE_CONFIG_SCHEMA_URL,
|
|
2474
|
+
mcp: {
|
|
2475
|
+
[shortName]: {
|
|
2476
|
+
type: "remote",
|
|
2477
|
+
url: urlInSnippet,
|
|
2478
|
+
headers: {
|
|
2479
|
+
Authorization: authHeader
|
|
2480
|
+
},
|
|
2481
|
+
enabled: true
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
null,
|
|
2486
|
+
2
|
|
2487
|
+
);
|
|
2488
|
+
} else if (platform === "codex-cli") {
|
|
2489
|
+
snippetText = `[mcp_servers.${shortName}]
|
|
2490
|
+
url = "${urlInSnippet}"
|
|
2491
|
+
bearer_token_env_var = "MULTICORN_API_KEY"
|
|
2492
|
+
`;
|
|
2266
2493
|
} else {
|
|
2267
2494
|
const urlKey = platform === "windsurf" ? "serverUrl" : "url";
|
|
2268
2495
|
snippetText = JSON.stringify(
|
|
@@ -2300,6 +2527,18 @@ mcpServers:
|
|
|
2300
2527
|
process.stderr.write(
|
|
2301
2528
|
"\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
|
|
2302
2529
|
);
|
|
2530
|
+
} else if (platform === "opencode") {
|
|
2531
|
+
process.stderr.write(
|
|
2532
|
+
"\n" + style.dim(
|
|
2533
|
+
"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."
|
|
2534
|
+
) + "\n\n"
|
|
2535
|
+
);
|
|
2536
|
+
} else if (platform === "codex-cli") {
|
|
2537
|
+
process.stderr.write(
|
|
2538
|
+
"\n" + style.dim(
|
|
2539
|
+
"Add this to ~/.codex/config.toml (create the file if it does not exist). Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
|
|
2540
|
+
) + "\n\n"
|
|
2541
|
+
);
|
|
2303
2542
|
} else if (platform === "github-copilot") {
|
|
2304
2543
|
process.stderr.write(
|
|
2305
2544
|
"\n" + style.dim(
|
|
@@ -2363,6 +2602,11 @@ mcpServers:
|
|
|
2363
2602
|
if (platform === "goose") {
|
|
2364
2603
|
process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
|
|
2365
2604
|
}
|
|
2605
|
+
if (platform === "opencode") {
|
|
2606
|
+
process.stderr.write(
|
|
2607
|
+
style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2366
2610
|
}
|
|
2367
2611
|
function agentDisplayNameDedupeKey(name) {
|
|
2368
2612
|
return name.trim().normalize("NFKC").toLowerCase();
|
|
@@ -2923,6 +3167,189 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2923
3167
|
setupSucceeded = true;
|
|
2924
3168
|
}
|
|
2925
3169
|
}
|
|
3170
|
+
} else if (selectedPlatform === "opencode") {
|
|
3171
|
+
const opencodeMode = await promptOpencodeIntegrationMode(ask);
|
|
3172
|
+
if (opencodeMode === "native") {
|
|
3173
|
+
try {
|
|
3174
|
+
await installOpenCodeNativePlugin();
|
|
3175
|
+
process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
|
|
3176
|
+
process.stderr.write(
|
|
3177
|
+
style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
|
|
3178
|
+
);
|
|
3179
|
+
process.stderr.write("\n");
|
|
3180
|
+
process.stderr.write(
|
|
3181
|
+
style.dim(
|
|
3182
|
+
"Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
|
|
3183
|
+
) + "\n"
|
|
3184
|
+
);
|
|
3185
|
+
process.stderr.write("\n");
|
|
3186
|
+
process.stderr.write(
|
|
3187
|
+
style.dim(
|
|
3188
|
+
"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."
|
|
3189
|
+
) + "\n"
|
|
3190
|
+
);
|
|
3191
|
+
configuredAgents.push({
|
|
3192
|
+
selection,
|
|
3193
|
+
platform: selectedPlatform,
|
|
3194
|
+
platformLabel: selectedLabel,
|
|
3195
|
+
agentName,
|
|
3196
|
+
opencodeCliIntegration: "native"
|
|
3197
|
+
});
|
|
3198
|
+
setupSucceeded = true;
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3201
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
3202
|
+
}
|
|
3203
|
+
} else {
|
|
3204
|
+
const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
|
|
3205
|
+
let proxyUrl = "";
|
|
3206
|
+
let created = false;
|
|
3207
|
+
while (!created) {
|
|
3208
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
3209
|
+
try {
|
|
3210
|
+
proxyUrl = await createProxyConfig(
|
|
3211
|
+
resolvedBaseUrl,
|
|
3212
|
+
apiKey,
|
|
3213
|
+
agentName,
|
|
3214
|
+
targetUrl,
|
|
3215
|
+
shortName,
|
|
3216
|
+
selectedPlatform,
|
|
3217
|
+
upstreamHeaders
|
|
3218
|
+
);
|
|
3219
|
+
spinner.stop(true, "Proxy config created!");
|
|
3220
|
+
created = true;
|
|
3221
|
+
} catch (error) {
|
|
3222
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3223
|
+
spinner.stop(false, detail);
|
|
3224
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
3225
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
3226
|
+
break;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
if (created && proxyUrl.length > 0) {
|
|
3231
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
3232
|
+
process.stderr.write(
|
|
3233
|
+
" " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
|
|
3234
|
+
);
|
|
3235
|
+
await applyHostedProxyMcpConfig(
|
|
3236
|
+
selectedPlatform,
|
|
3237
|
+
proxyUrl,
|
|
3238
|
+
shortName,
|
|
3239
|
+
apiKey,
|
|
3240
|
+
initWorkspacePath
|
|
3241
|
+
);
|
|
3242
|
+
process.stderr.write(
|
|
3243
|
+
"\n" + style.dim(
|
|
3244
|
+
"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."
|
|
3245
|
+
) + "\n"
|
|
3246
|
+
);
|
|
3247
|
+
configuredAgents.push({
|
|
3248
|
+
selection,
|
|
3249
|
+
platform: selectedPlatform,
|
|
3250
|
+
platformLabel: selectedLabel,
|
|
3251
|
+
agentName,
|
|
3252
|
+
shortName,
|
|
3253
|
+
proxyUrl,
|
|
3254
|
+
opencodeCliIntegration: "hosted"
|
|
3255
|
+
});
|
|
3256
|
+
setupSucceeded = true;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
} else if (selectedPlatform === "codex-cli") {
|
|
3260
|
+
const codexMode = await promptCodexCliIntegrationMode(ask);
|
|
3261
|
+
if (codexMode === "native") {
|
|
3262
|
+
try {
|
|
3263
|
+
await installCodexCliNativeHooks();
|
|
3264
|
+
process.stderr.write("\n" + style.bold("Shield Codex CLI hooks installed") + "\n\n");
|
|
3265
|
+
process.stderr.write(
|
|
3266
|
+
style.dim("Hook scripts: ") + style.cyan(getCodexCliHooksInstallDir()) + "\n"
|
|
3267
|
+
);
|
|
3268
|
+
process.stderr.write(
|
|
3269
|
+
style.dim("Hooks config: ") + style.cyan(getCodexHooksJsonPath()) + "\n"
|
|
3270
|
+
);
|
|
3271
|
+
process.stderr.write(
|
|
3272
|
+
style.dim("Feature flag: ") + style.cyan(getCodexConfigTomlPath()) + "\n"
|
|
3273
|
+
);
|
|
3274
|
+
process.stderr.write("\n");
|
|
3275
|
+
process.stderr.write(
|
|
3276
|
+
style.dim(
|
|
3277
|
+
"Codex hooks currently intercept terminal (Bash) commands only. File edits and MCP tool calls are not yet covered by hooks."
|
|
3278
|
+
) + "\n\n"
|
|
3279
|
+
);
|
|
3280
|
+
process.stderr.write(
|
|
3281
|
+
style.dim(
|
|
3282
|
+
"Start Codex CLI, then type /hooks to review the Shield hooks. Press 't' to trust each one before they can run."
|
|
3283
|
+
) + "\n"
|
|
3284
|
+
);
|
|
3285
|
+
configuredAgents.push({
|
|
3286
|
+
selection,
|
|
3287
|
+
platform: selectedPlatform,
|
|
3288
|
+
platformLabel: selectedLabel,
|
|
3289
|
+
agentName,
|
|
3290
|
+
codexCliIntegration: "native"
|
|
3291
|
+
});
|
|
3292
|
+
setupSucceeded = true;
|
|
3293
|
+
} catch (error) {
|
|
3294
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3295
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
3296
|
+
}
|
|
3297
|
+
} else {
|
|
3298
|
+
const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
|
|
3299
|
+
let proxyUrl = "";
|
|
3300
|
+
let created = false;
|
|
3301
|
+
while (!created) {
|
|
3302
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
3303
|
+
try {
|
|
3304
|
+
proxyUrl = await createProxyConfig(
|
|
3305
|
+
resolvedBaseUrl,
|
|
3306
|
+
apiKey,
|
|
3307
|
+
agentName,
|
|
3308
|
+
targetUrl,
|
|
3309
|
+
shortName,
|
|
3310
|
+
selectedPlatform,
|
|
3311
|
+
upstreamHeaders
|
|
3312
|
+
);
|
|
3313
|
+
spinner.stop(true, "Proxy config created!");
|
|
3314
|
+
created = true;
|
|
3315
|
+
} catch (error) {
|
|
3316
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3317
|
+
spinner.stop(false, detail);
|
|
3318
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
3319
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
3320
|
+
break;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (created && proxyUrl.length > 0) {
|
|
3325
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
3326
|
+
process.stderr.write(
|
|
3327
|
+
" " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
|
|
3328
|
+
);
|
|
3329
|
+
await applyHostedProxyMcpConfig(
|
|
3330
|
+
selectedPlatform,
|
|
3331
|
+
proxyUrl,
|
|
3332
|
+
shortName,
|
|
3333
|
+
apiKey,
|
|
3334
|
+
initWorkspacePath
|
|
3335
|
+
);
|
|
3336
|
+
process.stderr.write(
|
|
3337
|
+
"\n" + style.dim(
|
|
3338
|
+
"Add the TOML snippet above to ~/.codex/config.toml. Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
|
|
3339
|
+
) + "\n"
|
|
3340
|
+
);
|
|
3341
|
+
configuredAgents.push({
|
|
3342
|
+
selection,
|
|
3343
|
+
platform: selectedPlatform,
|
|
3344
|
+
platformLabel: selectedLabel,
|
|
3345
|
+
agentName,
|
|
3346
|
+
shortName,
|
|
3347
|
+
proxyUrl,
|
|
3348
|
+
codexCliIntegration: "hosted"
|
|
3349
|
+
});
|
|
3350
|
+
setupSucceeded = true;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
2926
3353
|
} else if (selectedPlatform === "cline") {
|
|
2927
3354
|
const clineMode = await promptClineIntegrationMode(ask);
|
|
2928
3355
|
if (clineMode === "native") {
|
|
@@ -3218,6 +3645,39 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3218
3645
|
"\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"
|
|
3219
3646
|
);
|
|
3220
3647
|
}
|
|
3648
|
+
const opencodeNativeConfigured = configuredAgents.some(
|
|
3649
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
|
|
3650
|
+
);
|
|
3651
|
+
const opencodeHostedConfigured = configuredAgents.some(
|
|
3652
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
|
|
3653
|
+
);
|
|
3654
|
+
if (opencodeNativeConfigured) {
|
|
3655
|
+
blocks.push(
|
|
3656
|
+
"\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"
|
|
3657
|
+
);
|
|
3658
|
+
}
|
|
3659
|
+
if (opencodeHostedConfigured) {
|
|
3660
|
+
const ocLabel = mcpPromptLabel2("opencode");
|
|
3661
|
+
blocks.push(
|
|
3662
|
+
"\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'
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
const codexNativeConfigured = configuredAgents.some(
|
|
3666
|
+
(a) => a.platform === "codex-cli" && a.codexCliIntegration === "native"
|
|
3667
|
+
);
|
|
3668
|
+
const codexHostedConfigured = configuredAgents.some(
|
|
3669
|
+
(a) => a.platform === "codex-cli" && a.codexCliIntegration === "hosted"
|
|
3670
|
+
);
|
|
3671
|
+
if (codexNativeConfigured) {
|
|
3672
|
+
blocks.push(
|
|
3673
|
+
"\n" + style.bold("Codex CLI (native)") + "\n \u2192 Start Codex CLI (run 'codex' in your terminal)\n \u2192 Type /hooks to review the Shield hooks - press 't' to trust each one\n \u2192 Once both hooks are trusted, try a Bash command - Shield will intercept and check permissions\n \u2192 Note: hooks currently cover terminal (Bash) commands only\n"
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
if (codexHostedConfigured) {
|
|
3677
|
+
blocks.push(
|
|
3678
|
+
"\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3221
3681
|
if (configuredPlatforms.has("other-mcp")) {
|
|
3222
3682
|
blocks.push(
|
|
3223
3683
|
"\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"
|
|
@@ -4123,7 +4583,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
|
|
|
4123
4583
|
|
|
4124
4584
|
// package.json
|
|
4125
4585
|
var package_default = {
|
|
4126
|
-
version: "1.
|
|
4586
|
+
version: "1.9.0"};
|
|
4127
4587
|
|
|
4128
4588
|
// src/package-meta.ts
|
|
4129
4589
|
var PACKAGE_VERSION = package_default.version;
|
package/dist/shield-extension.js
CHANGED
|
@@ -22354,6 +22354,18 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
|
22354
22354
|
{ slug: "windsurf", displayName: "Windsurf", section: "native" },
|
|
22355
22355
|
{ slug: "cline", displayName: "Cline", section: "native" },
|
|
22356
22356
|
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
|
|
22357
|
+
{
|
|
22358
|
+
slug: "opencode",
|
|
22359
|
+
displayName: "OpenCode",
|
|
22360
|
+
section: "native",
|
|
22361
|
+
prereqUrl: "https://opencode.ai"
|
|
22362
|
+
},
|
|
22363
|
+
{
|
|
22364
|
+
slug: "codex-cli",
|
|
22365
|
+
displayName: "Codex CLI",
|
|
22366
|
+
section: "native",
|
|
22367
|
+
prereqUrl: "https://github.com/openai/codex"
|
|
22368
|
+
},
|
|
22357
22369
|
{
|
|
22358
22370
|
slug: "cursor",
|
|
22359
22371
|
displayName: "Cursor",
|
|
@@ -22505,7 +22517,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22505
22517
|
|
|
22506
22518
|
// package.json
|
|
22507
22519
|
var package_default = {
|
|
22508
|
-
version: "1.
|
|
22520
|
+
version: "1.9.0"};
|
|
22509
22521
|
|
|
22510
22522
|
// src/package-meta.ts
|
|
22511
22523
|
var PACKAGE_VERSION = package_default.version;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multicorn-shield",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Multicorn AI Pty Ltd",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"plugins/windsurf",
|
|
41
41
|
"plugins/cline",
|
|
42
42
|
"plugins/gemini-cli",
|
|
43
|
+
"plugins/opencode",
|
|
43
44
|
"LICENSE",
|
|
44
45
|
"README.md",
|
|
45
46
|
"CHANGELOG.md"
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
"@anthropic-ai/mcpb": "^2.1.2",
|
|
77
78
|
"@eslint/js": "^9.19.0",
|
|
78
79
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
80
|
+
"@opencode-ai/plugin": "^1.14.48",
|
|
79
81
|
"@size-limit/file": "^11.1.6",
|
|
80
82
|
"@types/node": "^22.0.0",
|
|
81
83
|
"@vitest/coverage-v8": "^3.0.5",
|