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/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- Bump `version` in `package.json` before publishing to npm.
|
|
11
11
|
|
|
12
|
+
## [1.9.0] - 2026-05-12
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- OpenAI Codex CLI as a supported platform (native hooks + hosted proxy)
|
|
17
|
+
- Codex CLI hook scripts (PreToolUse, PostToolUse) for terminal command interception
|
|
18
|
+
- Codex CLI agent resolution and tool name mapping
|
|
19
|
+
- CLI wizard support for Codex CLI: native plugin install and hosted proxy TOML snippet
|
|
20
|
+
- `plugins/codex-cli/README.md` documenting hook script build and test workflow
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Codex config.toml feature flag updated from deprecated `codex_hooks` to `hooks`
|
|
25
|
+
- Config.toml migration: init now detects and replaces deprecated `codex_hooks` flag automatically
|
|
26
|
+
- Hook scripts reject plaintext HTTP for non-localhost API calls
|
|
27
|
+
- Config file permission warning when `~/.multicorn/config.json` is readable by other users
|
|
28
|
+
- Destructive command detection uses word-boundary matching instead of substring includes
|
|
29
|
+
- Unknown tool names default to restrictive `write` permission level instead of `execute`
|
|
30
|
+
- Audit log payloads size-bounded and secret patterns redacted before transmission
|
|
31
|
+
- Error messages sanitised: internal details hidden unless `MULTICORN_DEBUG` is set
|
|
32
|
+
- Test-mode polling escape hatch removed from production hook scripts
|
|
33
|
+
- Shared utility module extracted to eliminate duplication between hook scripts
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Error message prefix shortened from `[multicorn-shield]` to `[Shield]`
|
|
38
|
+
- "Audit trail" terminology replaced with "record the action" in user-facing messages
|
|
39
|
+
|
|
40
|
+
## [1.8.0] - 2026-05-11
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- OpenCode as a supported platform (native plugin + hosted proxy paths)
|
|
45
|
+
- Native Shield plugin for OpenCode (`plugins/opencode/multicorn-shield.ts`) using `tool.execute.before`/`tool.execute.after` hooks for permission checks and audit logging
|
|
46
|
+
- OpenCode in CLI init wizard with native plugin and hosted proxy integration modes
|
|
47
|
+
- Tool name mapping for OpenCode built-in tools (`bash`, `read`, `write`, `edit`, `apply_patch`, `glob`, `grep`, `list`, `webfetch`, `websearch`)
|
|
48
|
+
- Shell reload hint in CLI native plugin output for freshly installed tools
|
|
49
|
+
- `'opencode'` to `AGENT_PLATFORM_SLUGS`
|
|
50
|
+
|
|
12
51
|
## [1.7.0] - 2026-05-11
|
|
13
52
|
|
|
14
53
|
### Fixed
|
package/dist/index.cjs
CHANGED
package/dist/index.d.cts
CHANGED
|
@@ -12,7 +12,7 @@ import { LitElement, PropertyValues, HTMLTemplateResult } from 'lit';
|
|
|
12
12
|
/**
|
|
13
13
|
* Agent client platforms supported by hosted proxy and native hooks (aligned with API validation).
|
|
14
14
|
*/
|
|
15
|
-
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "continue-dev", "github-copilot", "goose", "kilo-code", "other-mcp", "github-actions", "unknown"];
|
|
15
|
+
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "continue-dev", "github-copilot", "goose", "kilo-code", "opencode", "codex-cli", "other-mcp", "github-actions", "unknown"];
|
|
16
16
|
type AgentPlatformSlug = (typeof AGENT_PLATFORM_SLUGS)[number];
|
|
17
17
|
/**
|
|
18
18
|
* Possible operational states for an agent.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { LitElement, PropertyValues, HTMLTemplateResult } from 'lit';
|
|
|
12
12
|
/**
|
|
13
13
|
* Agent client platforms supported by hosted proxy and native hooks (aligned with API validation).
|
|
14
14
|
*/
|
|
15
|
-
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "continue-dev", "github-copilot", "goose", "kilo-code", "other-mcp", "github-actions", "unknown"];
|
|
15
|
+
declare const AGENT_PLATFORM_SLUGS: readonly ["openclaw", "claude-code", "claude-desktop", "cursor", "windsurf", "cline", "gemini-cli", "continue-dev", "github-copilot", "goose", "kilo-code", "opencode", "codex-cli", "other-mcp", "github-actions", "unknown"];
|
|
16
16
|
type AgentPlatformSlug = (typeof AGENT_PLATFORM_SLUGS)[number];
|
|
17
17
|
/**
|
|
18
18
|
* Possible operational states for an agent.
|
package/dist/index.js
CHANGED
package/dist/multicorn-proxy.js
CHANGED
|
@@ -925,6 +925,115 @@ require(${JSON.stringify(destPost)});
|
|
|
925
925
|
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
926
926
|
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
927
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
|
+
}
|
|
944
|
+
function getCodexCliHooksInstallDir() {
|
|
945
|
+
return join(homedir(), ".multicorn", "codex-cli-hooks");
|
|
946
|
+
}
|
|
947
|
+
function getCodexConfigTomlPath() {
|
|
948
|
+
return join(homedir(), ".codex", "config.toml");
|
|
949
|
+
}
|
|
950
|
+
function getCodexHooksJsonPath() {
|
|
951
|
+
return join(homedir(), ".codex", "hooks.json");
|
|
952
|
+
}
|
|
953
|
+
async function installCodexCliNativeHooks() {
|
|
954
|
+
const root = multicornShieldPackageRoot();
|
|
955
|
+
const scriptsDir = join(root, "plugins", "codex-cli", "hooks", "scripts");
|
|
956
|
+
const preHook = join(scriptsDir, "pre-tool-use.cjs");
|
|
957
|
+
const postHook = join(scriptsDir, "post-tool-use.cjs");
|
|
958
|
+
const toolMap = join(scriptsDir, "codex-cli-tool-map.cjs");
|
|
959
|
+
const sharedHook = join(scriptsDir, "codex-cli-hooks-shared.cjs");
|
|
960
|
+
if (!existsSync(preHook) || !existsSync(postHook) || !existsSync(toolMap) || !existsSync(sharedHook)) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`Could not find Shield Codex CLI hook scripts at ${scriptsDir}. If you use npm, install the latest multicorn-shield package.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const destDir = getCodexCliHooksInstallDir();
|
|
966
|
+
await mkdir(destDir, { recursive: true });
|
|
967
|
+
await copyFile(preHook, join(destDir, "pre-tool-use.cjs"));
|
|
968
|
+
await copyFile(postHook, join(destDir, "post-tool-use.cjs"));
|
|
969
|
+
await copyFile(toolMap, join(destDir, "codex-cli-tool-map.cjs"));
|
|
970
|
+
await copyFile(sharedHook, join(destDir, "codex-cli-hooks-shared.cjs"));
|
|
971
|
+
const configTomlPath = getCodexConfigTomlPath();
|
|
972
|
+
await mkdir(join(homedir(), ".codex"), { recursive: true });
|
|
973
|
+
let tomlContent = "";
|
|
974
|
+
try {
|
|
975
|
+
tomlContent = await readFile(configTomlPath, "utf8");
|
|
976
|
+
} catch (err) {
|
|
977
|
+
if (!isErrnoException(err) || err.code !== "ENOENT") {
|
|
978
|
+
throw err;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (tomlContent.includes("codex_hooks")) {
|
|
982
|
+
tomlContent = tomlContent.replace(/codex_hooks/g, "hooks");
|
|
983
|
+
await writeFile(configTomlPath, tomlContent, "utf8");
|
|
984
|
+
}
|
|
985
|
+
if (!/\bhooks\s*=\s*true/.test(tomlContent)) {
|
|
986
|
+
tomlContent = tomlContent.includes("[features]") ? tomlContent.replace("[features]", "[features]\nhooks = true") : tomlContent.trimEnd() + (tomlContent.length > 0 ? "\n\n" : "") + "[features]\nhooks = true\n";
|
|
987
|
+
await writeFile(configTomlPath, tomlContent, "utf8");
|
|
988
|
+
}
|
|
989
|
+
const hooksConfig = {
|
|
990
|
+
hooks: {
|
|
991
|
+
PreToolUse: [
|
|
992
|
+
{
|
|
993
|
+
matcher: "",
|
|
994
|
+
hooks: [
|
|
995
|
+
{
|
|
996
|
+
type: "command",
|
|
997
|
+
command: `node ${join(destDir, "pre-tool-use.cjs")}`,
|
|
998
|
+
statusMessage: "Checking Shield permissions"
|
|
999
|
+
}
|
|
1000
|
+
]
|
|
1001
|
+
}
|
|
1002
|
+
],
|
|
1003
|
+
PostToolUse: [
|
|
1004
|
+
{
|
|
1005
|
+
matcher: "",
|
|
1006
|
+
hooks: [
|
|
1007
|
+
{
|
|
1008
|
+
type: "command",
|
|
1009
|
+
command: `node ${join(destDir, "post-tool-use.cjs")}`,
|
|
1010
|
+
timeout: 30,
|
|
1011
|
+
statusMessage: "Logging to Shield"
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
await writeFile(getCodexHooksJsonPath(), JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
|
|
1019
|
+
}
|
|
1020
|
+
async function promptCodexCliIntegrationMode(ask) {
|
|
1021
|
+
process.stderr.write("\n" + style.bold("Codex CLI integration") + "\n");
|
|
1022
|
+
process.stderr.write(
|
|
1023
|
+
" " + style.violet("1") + ". Native plugin - Shield checks terminal (Bash) commands via Codex Hooks\n"
|
|
1024
|
+
);
|
|
1025
|
+
process.stderr.write(
|
|
1026
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via config.toml\n"
|
|
1027
|
+
);
|
|
1028
|
+
let choice = 0;
|
|
1029
|
+
while (choice === 0) {
|
|
1030
|
+
const input = await ask("Choose integration (1-2): ");
|
|
1031
|
+
const num = parseInt(input.trim(), 10);
|
|
1032
|
+
if (num === 1) choice = 1;
|
|
1033
|
+
if (num === 2) choice = 2;
|
|
1034
|
+
}
|
|
1035
|
+
return choice === 1 ? "native" : "hosted";
|
|
1036
|
+
}
|
|
928
1037
|
async function promptClineIntegrationMode(ask) {
|
|
929
1038
|
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
930
1039
|
process.stderr.write(
|
|
@@ -1284,6 +1393,23 @@ async function promptGeminiCliIntegrationMode(ask) {
|
|
|
1284
1393
|
}
|
|
1285
1394
|
return choice === 1 ? "native" : "hosted";
|
|
1286
1395
|
}
|
|
1396
|
+
async function promptOpencodeIntegrationMode(ask) {
|
|
1397
|
+
process.stderr.write("\n" + style.bold("OpenCode integration") + "\n");
|
|
1398
|
+
process.stderr.write(
|
|
1399
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Shield checks primary-agent tool execution via OpenCode Hooks\n"
|
|
1400
|
+
);
|
|
1401
|
+
process.stderr.write(
|
|
1402
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP server traffic via opencode.json (full subagent coverage when tools use MCP through Shield)\n"
|
|
1403
|
+
);
|
|
1404
|
+
let choice = 0;
|
|
1405
|
+
while (choice === 0) {
|
|
1406
|
+
const input = await ask("Choose integration (1-2): ");
|
|
1407
|
+
const num = parseInt(input.trim(), 10);
|
|
1408
|
+
if (num === 1) choice = 1;
|
|
1409
|
+
if (num === 2) choice = 2;
|
|
1410
|
+
}
|
|
1411
|
+
return choice === 1 ? "native" : "hosted";
|
|
1412
|
+
}
|
|
1287
1413
|
function getClaudeDesktopConfigPath() {
|
|
1288
1414
|
switch (process.platform) {
|
|
1289
1415
|
case "win32":
|
|
@@ -1828,6 +1954,49 @@ async function mergeKiloCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKe
|
|
|
1828
1954
|
}
|
|
1829
1955
|
return "ok";
|
|
1830
1956
|
}
|
|
1957
|
+
async function injectOpencodeSchemaIntoConfigIfMissing(filePath) {
|
|
1958
|
+
try {
|
|
1959
|
+
const raw = await readFile(filePath, "utf8");
|
|
1960
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1961
|
+
let parsed;
|
|
1962
|
+
try {
|
|
1963
|
+
parsed = JSON.parse(stripped);
|
|
1964
|
+
} catch {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
1968
|
+
const root = parsed;
|
|
1969
|
+
const existingSchema = root["$schema"];
|
|
1970
|
+
if (typeof existingSchema === "string" && existingSchema.length > 0) return;
|
|
1971
|
+
root["$schema"] = OPENCODE_CONFIG_SCHEMA_URL;
|
|
1972
|
+
await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
async function mergeOpenCodeProjectMcp(workspacePath, shortName, proxyUrl, apiKey) {
|
|
1977
|
+
const filePath = join(workspacePath, "opencode.json");
|
|
1978
|
+
const result = await mergeTopLevelKeyedJsonFile(
|
|
1979
|
+
filePath,
|
|
1980
|
+
"mcp",
|
|
1981
|
+
shortName,
|
|
1982
|
+
{
|
|
1983
|
+
type: "remote",
|
|
1984
|
+
url: proxyUrl,
|
|
1985
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1986
|
+
enabled: true
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
stripJsonComments: true,
|
|
1990
|
+
onExisting: "skip"
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
if (result === "parse-error") return "parse-error";
|
|
1994
|
+
await injectOpencodeSchemaIntoConfigIfMissing(filePath);
|
|
1995
|
+
if (result === "ok") {
|
|
1996
|
+
await warnIfApiKeyFileNotGitignored(workspacePath, "opencode.json");
|
|
1997
|
+
}
|
|
1998
|
+
return "ok";
|
|
1999
|
+
}
|
|
1831
2000
|
function printHostedProxyJsonParseWarning(filePath) {
|
|
1832
2001
|
process.stderr.write(
|
|
1833
2002
|
style.yellow("\u26A0") + " Could not parse JSON at " + style.cyan(filePath) + style.dim(" - showing paste snippet instead.") + "\n"
|
|
@@ -1870,6 +2039,13 @@ function printHostedProxyPostWriteHints(platform, shortName) {
|
|
|
1870
2039
|
style.dim("Restart Kilo Code or reload the window so it picks up .kilo/kilo.jsonc.") + "\n"
|
|
1871
2040
|
);
|
|
1872
2041
|
}
|
|
2042
|
+
if (platform === "opencode") {
|
|
2043
|
+
process.stderr.write(
|
|
2044
|
+
style.dim(
|
|
2045
|
+
"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."
|
|
2046
|
+
) + "\n"
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
1873
2049
|
}
|
|
1874
2050
|
async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey, workspacePath) {
|
|
1875
2051
|
const authHeader = `Bearer ${apiKey}`;
|
|
@@ -1966,6 +2142,19 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
|
|
|
1966
2142
|
if (result === "parse-error") {
|
|
1967
2143
|
printHostedProxyJsonParseWarning(join(workspacePath, ".kilo", "kilo.jsonc"));
|
|
1968
2144
|
}
|
|
2145
|
+
} else if (platform === "opencode") {
|
|
2146
|
+
result = await mergeOpenCodeProjectMcp(
|
|
2147
|
+
workspacePath,
|
|
2148
|
+
shortName,
|
|
2149
|
+
proxyUrlWithKeyWhenNeeded,
|
|
2150
|
+
apiKey
|
|
2151
|
+
);
|
|
2152
|
+
if (result === "parse-error") {
|
|
2153
|
+
printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
|
|
2154
|
+
}
|
|
2155
|
+
} else if (platform === "codex-cli") {
|
|
2156
|
+
printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
|
|
2157
|
+
return;
|
|
1969
2158
|
} else if (platform === "continue-dev") {
|
|
1970
2159
|
result = await mergeContinueHostedMcp(
|
|
1971
2160
|
workspacePath,
|
|
@@ -2100,7 +2289,9 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
2100
2289
|
"kilo-code",
|
|
2101
2290
|
"github-copilot",
|
|
2102
2291
|
"continue-dev",
|
|
2103
|
-
"goose"
|
|
2292
|
+
"goose",
|
|
2293
|
+
"opencode",
|
|
2294
|
+
"codex-cli"
|
|
2104
2295
|
]);
|
|
2105
2296
|
const usesInlineKey = hostedInlinePlatforms.has(platform);
|
|
2106
2297
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
@@ -2183,6 +2374,29 @@ mcpServers:
|
|
|
2183
2374
|
null,
|
|
2184
2375
|
2
|
|
2185
2376
|
);
|
|
2377
|
+
} else if (platform === "opencode") {
|
|
2378
|
+
snippetText = JSON.stringify(
|
|
2379
|
+
{
|
|
2380
|
+
$schema: OPENCODE_CONFIG_SCHEMA_URL,
|
|
2381
|
+
mcp: {
|
|
2382
|
+
[shortName]: {
|
|
2383
|
+
type: "remote",
|
|
2384
|
+
url: urlInSnippet,
|
|
2385
|
+
headers: {
|
|
2386
|
+
Authorization: authHeader
|
|
2387
|
+
},
|
|
2388
|
+
enabled: true
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
},
|
|
2392
|
+
null,
|
|
2393
|
+
2
|
|
2394
|
+
);
|
|
2395
|
+
} else if (platform === "codex-cli") {
|
|
2396
|
+
snippetText = `[mcp_servers.${shortName}]
|
|
2397
|
+
url = "${urlInSnippet}"
|
|
2398
|
+
bearer_token_env_var = "MULTICORN_API_KEY"
|
|
2399
|
+
`;
|
|
2186
2400
|
} else {
|
|
2187
2401
|
const urlKey = platform === "windsurf" ? "serverUrl" : "url";
|
|
2188
2402
|
snippetText = JSON.stringify(
|
|
@@ -2220,6 +2434,18 @@ mcpServers:
|
|
|
2220
2434
|
process.stderr.write(
|
|
2221
2435
|
"\n" + style.dim(`Add this to ${join(resolve(process.cwd()), ".kilo", "kilo.jsonc")}:`) + "\n\n"
|
|
2222
2436
|
);
|
|
2437
|
+
} else if (platform === "opencode") {
|
|
2438
|
+
process.stderr.write(
|
|
2439
|
+
"\n" + style.dim(
|
|
2440
|
+
"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."
|
|
2441
|
+
) + "\n\n"
|
|
2442
|
+
);
|
|
2443
|
+
} else if (platform === "codex-cli") {
|
|
2444
|
+
process.stderr.write(
|
|
2445
|
+
"\n" + style.dim(
|
|
2446
|
+
"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."
|
|
2447
|
+
) + "\n\n"
|
|
2448
|
+
);
|
|
2223
2449
|
} else if (platform === "github-copilot") {
|
|
2224
2450
|
process.stderr.write(
|
|
2225
2451
|
"\n" + style.dim(
|
|
@@ -2283,6 +2509,11 @@ mcpServers:
|
|
|
2283
2509
|
if (platform === "goose") {
|
|
2284
2510
|
process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
|
|
2285
2511
|
}
|
|
2512
|
+
if (platform === "opencode") {
|
|
2513
|
+
process.stderr.write(
|
|
2514
|
+
style.dim("Restart OpenCode or start a new session after saving opencode.json.") + "\n"
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2286
2517
|
}
|
|
2287
2518
|
function agentDisplayNameDedupeKey(name) {
|
|
2288
2519
|
return name.trim().normalize("NFKC").toLowerCase();
|
|
@@ -2842,6 +3073,189 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2842
3073
|
setupSucceeded = true;
|
|
2843
3074
|
}
|
|
2844
3075
|
}
|
|
3076
|
+
} else if (selectedPlatform === "opencode") {
|
|
3077
|
+
const opencodeMode = await promptOpencodeIntegrationMode(ask);
|
|
3078
|
+
if (opencodeMode === "native") {
|
|
3079
|
+
try {
|
|
3080
|
+
await installOpenCodeNativePlugin();
|
|
3081
|
+
process.stderr.write("\n" + style.bold("Shield OpenCode plugin installed") + "\n\n");
|
|
3082
|
+
process.stderr.write(
|
|
3083
|
+
style.dim("Plugin file: ") + style.cyan(getOpenCodeGlobalPluginsDir()) + "\n"
|
|
3084
|
+
);
|
|
3085
|
+
process.stderr.write("\n");
|
|
3086
|
+
process.stderr.write(
|
|
3087
|
+
style.dim(
|
|
3088
|
+
"Shield plugin saved under ~/.config/opencode/plugins/. Restart OpenCode. Every tool call from the primary agent will be checked by Shield."
|
|
3089
|
+
) + "\n"
|
|
3090
|
+
);
|
|
3091
|
+
process.stderr.write("\n");
|
|
3092
|
+
process.stderr.write(
|
|
3093
|
+
style.dim(
|
|
3094
|
+
"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."
|
|
3095
|
+
) + "\n"
|
|
3096
|
+
);
|
|
3097
|
+
configuredAgents.push({
|
|
3098
|
+
selection,
|
|
3099
|
+
platform: selectedPlatform,
|
|
3100
|
+
platformLabel: selectedLabel,
|
|
3101
|
+
agentName,
|
|
3102
|
+
opencodeCliIntegration: "native"
|
|
3103
|
+
});
|
|
3104
|
+
setupSucceeded = true;
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3107
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
3108
|
+
}
|
|
3109
|
+
} else {
|
|
3110
|
+
const { targetUrl, shortName, upstreamHeaders } = await promptProxyConfig(ask, agentName);
|
|
3111
|
+
let proxyUrl = "";
|
|
3112
|
+
let created = false;
|
|
3113
|
+
while (!created) {
|
|
3114
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
3115
|
+
try {
|
|
3116
|
+
proxyUrl = await createProxyConfig(
|
|
3117
|
+
resolvedBaseUrl,
|
|
3118
|
+
apiKey,
|
|
3119
|
+
agentName,
|
|
3120
|
+
targetUrl,
|
|
3121
|
+
shortName,
|
|
3122
|
+
selectedPlatform,
|
|
3123
|
+
upstreamHeaders
|
|
3124
|
+
);
|
|
3125
|
+
spinner.stop(true, "Proxy config created!");
|
|
3126
|
+
created = true;
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3129
|
+
spinner.stop(false, detail);
|
|
3130
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
3131
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
3132
|
+
break;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (created && proxyUrl.length > 0) {
|
|
3137
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
3138
|
+
process.stderr.write(
|
|
3139
|
+
" " + style.cyan(formatHostedProxyUrlForStderr(selectedPlatform, proxyUrl, apiKey)) + "\n"
|
|
3140
|
+
);
|
|
3141
|
+
await applyHostedProxyMcpConfig(
|
|
3142
|
+
selectedPlatform,
|
|
3143
|
+
proxyUrl,
|
|
3144
|
+
shortName,
|
|
3145
|
+
apiKey,
|
|
3146
|
+
initWorkspacePath
|
|
3147
|
+
);
|
|
3148
|
+
process.stderr.write(
|
|
3149
|
+
"\n" + style.dim(
|
|
3150
|
+
"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."
|
|
3151
|
+
) + "\n"
|
|
3152
|
+
);
|
|
3153
|
+
configuredAgents.push({
|
|
3154
|
+
selection,
|
|
3155
|
+
platform: selectedPlatform,
|
|
3156
|
+
platformLabel: selectedLabel,
|
|
3157
|
+
agentName,
|
|
3158
|
+
shortName,
|
|
3159
|
+
proxyUrl,
|
|
3160
|
+
opencodeCliIntegration: "hosted"
|
|
3161
|
+
});
|
|
3162
|
+
setupSucceeded = true;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
} else if (selectedPlatform === "codex-cli") {
|
|
3166
|
+
const codexMode = await promptCodexCliIntegrationMode(ask);
|
|
3167
|
+
if (codexMode === "native") {
|
|
3168
|
+
try {
|
|
3169
|
+
await installCodexCliNativeHooks();
|
|
3170
|
+
process.stderr.write("\n" + style.bold("Shield Codex CLI hooks installed") + "\n\n");
|
|
3171
|
+
process.stderr.write(
|
|
3172
|
+
style.dim("Hook scripts: ") + style.cyan(getCodexCliHooksInstallDir()) + "\n"
|
|
3173
|
+
);
|
|
3174
|
+
process.stderr.write(
|
|
3175
|
+
style.dim("Hooks config: ") + style.cyan(getCodexHooksJsonPath()) + "\n"
|
|
3176
|
+
);
|
|
3177
|
+
process.stderr.write(
|
|
3178
|
+
style.dim("Feature flag: ") + style.cyan(getCodexConfigTomlPath()) + "\n"
|
|
3179
|
+
);
|
|
3180
|
+
process.stderr.write("\n");
|
|
3181
|
+
process.stderr.write(
|
|
3182
|
+
style.dim(
|
|
3183
|
+
"Codex hooks currently intercept terminal (Bash) commands only. File edits and MCP tool calls are not yet covered by hooks."
|
|
3184
|
+
) + "\n\n"
|
|
3185
|
+
);
|
|
3186
|
+
process.stderr.write(
|
|
3187
|
+
style.dim(
|
|
3188
|
+
"Start Codex CLI, then type /hooks to review the Shield hooks. Press 't' to trust each one before they can run."
|
|
3189
|
+
) + "\n"
|
|
3190
|
+
);
|
|
3191
|
+
configuredAgents.push({
|
|
3192
|
+
selection,
|
|
3193
|
+
platform: selectedPlatform,
|
|
3194
|
+
platformLabel: selectedLabel,
|
|
3195
|
+
agentName,
|
|
3196
|
+
codexCliIntegration: "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 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."
|
|
3245
|
+
) + "\n"
|
|
3246
|
+
);
|
|
3247
|
+
configuredAgents.push({
|
|
3248
|
+
selection,
|
|
3249
|
+
platform: selectedPlatform,
|
|
3250
|
+
platformLabel: selectedLabel,
|
|
3251
|
+
agentName,
|
|
3252
|
+
shortName,
|
|
3253
|
+
proxyUrl,
|
|
3254
|
+
codexCliIntegration: "hosted"
|
|
3255
|
+
});
|
|
3256
|
+
setupSucceeded = true;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
2845
3259
|
} else if (selectedPlatform === "cline") {
|
|
2846
3260
|
const clineMode = await promptClineIntegrationMode(ask);
|
|
2847
3261
|
if (clineMode === "native") {
|
|
@@ -3137,6 +3551,39 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3137
3551
|
"\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"
|
|
3138
3552
|
);
|
|
3139
3553
|
}
|
|
3554
|
+
const opencodeNativeConfigured = configuredAgents.some(
|
|
3555
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "native"
|
|
3556
|
+
);
|
|
3557
|
+
const opencodeHostedConfigured = configuredAgents.some(
|
|
3558
|
+
(a) => a.platform === "opencode" && a.opencodeCliIntegration === "hosted"
|
|
3559
|
+
);
|
|
3560
|
+
if (opencodeNativeConfigured) {
|
|
3561
|
+
blocks.push(
|
|
3562
|
+
"\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"
|
|
3563
|
+
);
|
|
3564
|
+
}
|
|
3565
|
+
if (opencodeHostedConfigured) {
|
|
3566
|
+
const ocLabel = mcpPromptLabel2("opencode");
|
|
3567
|
+
blocks.push(
|
|
3568
|
+
"\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'
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
const codexNativeConfigured = configuredAgents.some(
|
|
3572
|
+
(a) => a.platform === "codex-cli" && a.codexCliIntegration === "native"
|
|
3573
|
+
);
|
|
3574
|
+
const codexHostedConfigured = configuredAgents.some(
|
|
3575
|
+
(a) => a.platform === "codex-cli" && a.codexCliIntegration === "hosted"
|
|
3576
|
+
);
|
|
3577
|
+
if (codexNativeConfigured) {
|
|
3578
|
+
blocks.push(
|
|
3579
|
+
"\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"
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
if (codexHostedConfigured) {
|
|
3583
|
+
blocks.push(
|
|
3584
|
+
"\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"
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3140
3587
|
if (configuredPlatforms.has("other-mcp")) {
|
|
3141
3588
|
blocks.push(
|
|
3142
3589
|
"\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"
|
|
@@ -3156,7 +3603,7 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
3156
3603
|
}
|
|
3157
3604
|
return lastConfig;
|
|
3158
3605
|
}
|
|
3159
|
-
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;
|
|
3606
|
+
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;
|
|
3160
3607
|
var init_config = __esm({
|
|
3161
3608
|
"src/proxy/config.ts"() {
|
|
3162
3609
|
init_consent();
|
|
@@ -3196,6 +3643,18 @@ var init_config = __esm({
|
|
|
3196
3643
|
{ slug: "windsurf", displayName: "Windsurf", section: "native" },
|
|
3197
3644
|
{ slug: "cline", displayName: "Cline", section: "native" },
|
|
3198
3645
|
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
|
|
3646
|
+
{
|
|
3647
|
+
slug: "opencode",
|
|
3648
|
+
displayName: "OpenCode",
|
|
3649
|
+
section: "native",
|
|
3650
|
+
prereqUrl: "https://opencode.ai"
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
slug: "codex-cli",
|
|
3654
|
+
displayName: "Codex CLI",
|
|
3655
|
+
section: "native",
|
|
3656
|
+
prereqUrl: "https://github.com/openai/codex"
|
|
3657
|
+
},
|
|
3199
3658
|
{
|
|
3200
3659
|
slug: "cursor",
|
|
3201
3660
|
displayName: "Cursor",
|
|
@@ -3256,6 +3715,7 @@ var init_config = __esm({
|
|
|
3256
3715
|
"continue-dev",
|
|
3257
3716
|
"goose"
|
|
3258
3717
|
]);
|
|
3718
|
+
OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
3259
3719
|
DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
|
|
3260
3720
|
}
|
|
3261
3721
|
});
|
|
@@ -4200,7 +4660,7 @@ var init_package = __esm({
|
|
|
4200
4660
|
"package.json"() {
|
|
4201
4661
|
package_default = {
|
|
4202
4662
|
name: "multicorn-shield",
|
|
4203
|
-
version: "1.
|
|
4663
|
+
version: "1.9.0",
|
|
4204
4664
|
description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
|
|
4205
4665
|
license: "MIT",
|
|
4206
4666
|
author: "Multicorn AI Pty Ltd",
|
|
@@ -4240,6 +4700,7 @@ var init_package = __esm({
|
|
|
4240
4700
|
"plugins/windsurf",
|
|
4241
4701
|
"plugins/cline",
|
|
4242
4702
|
"plugins/gemini-cli",
|
|
4703
|
+
"plugins/opencode",
|
|
4243
4704
|
"LICENSE",
|
|
4244
4705
|
"README.md",
|
|
4245
4706
|
"CHANGELOG.md"
|
|
@@ -4298,6 +4759,7 @@ var init_package = __esm({
|
|
|
4298
4759
|
"@anthropic-ai/mcpb": "^2.1.2",
|
|
4299
4760
|
"@eslint/js": "^9.19.0",
|
|
4300
4761
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
4762
|
+
"@opencode-ai/plugin": "^1.14.48",
|
|
4301
4763
|
"@size-limit/file": "^11.1.6",
|
|
4302
4764
|
"@types/node": "^22.0.0",
|
|
4303
4765
|
"@vitest/coverage-v8": "^3.0.5",
|