multicorn-shield 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +10 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/multicorn-proxy.js +250 -201
- package/dist/multicorn-shield.js +246 -199
- package/dist/openclaw-hook/HOOK.md +5 -3
- package/dist/openclaw-hook/handler.js +1 -1
- package/dist/openclaw-plugin/multicorn-shield.js +28 -7
- package/dist/openclaw-plugin/openclaw.plugin.json +1 -1
- package/dist/shield-extension.js +3 -12
- package/package.json +4 -2
- package/plugins/windsurf/hooks/scripts/pre-action.cjs +126 -17
|
@@ -33,9 +33,11 @@ metadata:
|
|
|
33
33
|
> "entries": {
|
|
34
34
|
> "multicorn-shield": {
|
|
35
35
|
> "enabled": true,
|
|
36
|
-
> "
|
|
37
|
-
> "
|
|
38
|
-
> "
|
|
36
|
+
> "config": {
|
|
37
|
+
> "apiKey": "${MULTICORN_API_KEY}",
|
|
38
|
+
> "baseUrl": "https://api.multicorn.ai",
|
|
39
|
+
> "agentName": "openclaw",
|
|
40
|
+
> "failMode": "closed"
|
|
39
41
|
> }
|
|
40
42
|
> }
|
|
41
43
|
> }
|
|
@@ -184,7 +184,7 @@ function handleHttpError(status, logger, retryDelaySeconds) {
|
|
|
184
184
|
if (status === 401 || status === 403) {
|
|
185
185
|
if (!authErrorLogged) {
|
|
186
186
|
authErrorLogged = true;
|
|
187
|
-
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in
|
|
187
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in ~/.multicorn/config.json (from npx multicorn-shield init) or ~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.config.apiKey. Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
188
188
|
process.stderr.write(`${errorMsg}
|
|
189
189
|
`);
|
|
190
190
|
}
|
|
@@ -189,7 +189,7 @@ function handleHttpError(status, logger, retryDelaySeconds) {
|
|
|
189
189
|
if (status === 401 || status === 403) {
|
|
190
190
|
if (!authErrorLogged) {
|
|
191
191
|
authErrorLogged = true;
|
|
192
|
-
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in
|
|
192
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in ~/.multicorn/config.json (from npx multicorn-shield init) or ~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.config.apiKey. Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
193
193
|
logger?.error(errorMsg);
|
|
194
194
|
process.stderr.write(`${errorMsg}
|
|
195
195
|
`);
|
|
@@ -491,20 +491,41 @@ function agentNameFromOpenclawPlatform(cfg) {
|
|
|
491
491
|
}
|
|
492
492
|
return void 0;
|
|
493
493
|
}
|
|
494
|
+
var ENV_REF_PATTERN = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
|
|
495
|
+
function resolveConfigString(value) {
|
|
496
|
+
const raw = asString(value);
|
|
497
|
+
if (raw === void 0) return void 0;
|
|
498
|
+
const match = ENV_REF_PATTERN.exec(raw.trim());
|
|
499
|
+
if (match === null) return raw;
|
|
500
|
+
const envName = match[1];
|
|
501
|
+
if (envName === void 0) return void 0;
|
|
502
|
+
const fromEnv = process.env[envName];
|
|
503
|
+
return typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : void 0;
|
|
504
|
+
}
|
|
505
|
+
function parseFailMode(value) {
|
|
506
|
+
if (value === "open") return "open";
|
|
507
|
+
if (value === "closed") return "closed";
|
|
508
|
+
return "closed";
|
|
509
|
+
}
|
|
494
510
|
function readConfig() {
|
|
495
511
|
const pc = pluginConfig ?? {};
|
|
496
|
-
const
|
|
497
|
-
const
|
|
512
|
+
const fromFileKey = asString(cachedMulticornConfig?.apiKey);
|
|
513
|
+
const fromPluginKey = resolveConfigString(pc["apiKey"]);
|
|
514
|
+
const fromEnvKey = asString(process.env["MULTICORN_API_KEY"]);
|
|
515
|
+
let apiKey = fromFileKey ?? fromPluginKey ?? fromEnvKey ?? "";
|
|
516
|
+
const fromPluginBase = resolveConfigString(pc["baseUrl"]);
|
|
517
|
+
const fromFileBase = asString(cachedMulticornConfig?.baseUrl);
|
|
518
|
+
const fromEnvBase = asString(process.env["MULTICORN_BASE_URL"]);
|
|
519
|
+
const baseUrl = fromPluginBase ?? fromFileBase ?? fromEnvBase ?? "https://api.multicorn.ai";
|
|
498
520
|
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? agentNameFromOpenclawPlatform(cachedMulticornConfig) ?? asString(cachedMulticornConfig?.agentName) ?? null;
|
|
499
|
-
const failMode = "
|
|
500
|
-
let apiKey = resolvedApiKey;
|
|
521
|
+
const failMode = parseFailMode(pc["failMode"]);
|
|
501
522
|
if (apiKey.length > 0 && (!apiKey.startsWith("mcs_") || apiKey.length < 16)) {
|
|
502
523
|
pluginLogger?.error(
|
|
503
524
|
"Invalid API key format. Key must start with mcs_ and be at least 16 characters."
|
|
504
525
|
);
|
|
505
526
|
apiKey = "";
|
|
506
527
|
}
|
|
507
|
-
return { apiKey, baseUrl
|
|
528
|
+
return { apiKey, baseUrl, agentName, failMode };
|
|
508
529
|
}
|
|
509
530
|
function asString(value) {
|
|
510
531
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -919,4 +940,4 @@ function resetState() {
|
|
|
919
940
|
hasLoggedFirstAction = false;
|
|
920
941
|
}
|
|
921
942
|
|
|
922
|
-
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
|
|
943
|
+
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName, resolveConfigString };
|
package/dist/shield-extension.js
CHANGED
|
@@ -22405,18 +22405,9 @@ var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
|
22405
22405
|
},
|
|
22406
22406
|
{ slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
|
|
22407
22407
|
];
|
|
22408
|
-
(() =>
|
|
22409
|
-
const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
|
|
22410
|
-
platform: e.slug,
|
|
22411
|
-
label: e.displayName
|
|
22412
|
-
}));
|
|
22413
|
-
return [
|
|
22414
|
-
{ title: "Recommended (native plugin)", items: itemsFor("native") },
|
|
22415
|
-
{ title: "Hosted proxy (MCP only)", items: itemsFor("hosted") }
|
|
22416
|
-
];
|
|
22417
|
-
})();
|
|
22408
|
+
var INIT_WIZARD_PICKER_NATIVE_SLUGS = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === "native").map((e) => e.slug);
|
|
22418
22409
|
Object.fromEntries(
|
|
22419
|
-
|
|
22410
|
+
INIT_WIZARD_PICKER_NATIVE_SLUGS.map((slug, i) => [i + 1, slug])
|
|
22420
22411
|
);
|
|
22421
22412
|
|
|
22422
22413
|
// src/extension/config-reader.ts
|
|
@@ -22518,7 +22509,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
|
|
|
22518
22509
|
|
|
22519
22510
|
// package.json
|
|
22520
22511
|
var package_default = {
|
|
22521
|
-
version: "1.
|
|
22512
|
+
version: "1.12.0"};
|
|
22522
22513
|
|
|
22523
22514
|
// src/package-meta.ts
|
|
22524
22515
|
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.12.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",
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"prepare": "husky || true",
|
|
80
80
|
"release:patch": "npm version patch && pnpm publish",
|
|
81
81
|
"release:minor": "npm version minor && pnpm publish",
|
|
82
|
-
"release:major": "npm version major && pnpm publish"
|
|
82
|
+
"release:major": "npm version major && pnpm publish",
|
|
83
|
+
"local": "pnpm build &&node dist/multicorn-shield.js init --base-url http://localhost:8080"
|
|
83
84
|
},
|
|
84
85
|
"lint-staged": {
|
|
85
86
|
"*.ts": [
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
"@size-limit/file": "^11.1.6",
|
|
108
109
|
"@types/node": "^22.0.0",
|
|
109
110
|
"@vitest/coverage-v8": "^3.0.5",
|
|
111
|
+
"ajv": "^8.18.0",
|
|
110
112
|
"eslint": "^9.19.0",
|
|
111
113
|
"eslint-config-prettier": "^10.0.1",
|
|
112
114
|
"eslint-plugin-unicorn": "^57.0.0",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windsurf Cascade pre-hook: permission check before read, write, terminal, or MCP tool use.
|
|
3
3
|
* Routes by stdin JSON field agent_action_name (see Windsurf Cascade Hooks docs).
|
|
4
|
-
* Fail-closed on API errors once config is loaded
|
|
4
|
+
* Fail-closed on API errors once config is loaded or Windsurf hooks are installed.
|
|
5
|
+
* Fail-open only when Shield is not configured (no config file and no installed hook copy).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
"use strict";
|
|
@@ -110,12 +111,83 @@ function resolveWindsurfAgentName(obj) {
|
|
|
110
111
|
return pickAgentNameForPlatform(obj, "windsurf", process.cwd());
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function multicornConfigPath() {
|
|
118
|
+
return path.join(os.homedir(), ".multicorn", "config.json");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function configFileExists() {
|
|
125
|
+
try {
|
|
126
|
+
fs.accessSync(multicornConfigPath());
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function isShieldWindsurfHooksInstalled() {
|
|
137
|
+
try {
|
|
138
|
+
fs.accessSync(path.join(os.homedir(), ".multicorn", "windsurf-hooks", "pre-action.cjs"));
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
function shouldEnforceGovernance() {
|
|
149
|
+
return configFileExists() || isShieldWindsurfHooksInstalled();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} baseUrl
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
function apiHostFromBaseUrl(baseUrl) {
|
|
157
|
+
try {
|
|
158
|
+
return new URL(baseUrl).host;
|
|
159
|
+
} catch {
|
|
160
|
+
return "unknown";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} agentActionName
|
|
166
|
+
* @param {string} agentName
|
|
167
|
+
* @param {string} baseUrl
|
|
168
|
+
*/
|
|
169
|
+
function logInvocation(agentActionName, agentName, baseUrl) {
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`${LOG_PREFIX} check event=${agentActionName} agent=${agentName} api=${apiHostFromBaseUrl(baseUrl)}\n`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} cause
|
|
177
|
+
* @param {string} fix
|
|
178
|
+
* @returns {never}
|
|
179
|
+
*/
|
|
180
|
+
function blockMisconfigured(cause, fix) {
|
|
181
|
+
process.stderr.write(`${LOG_PREFIX} Action blocked: ${cause}\n ${fix}\n`);
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
|
|
113
185
|
/**
|
|
114
186
|
* @returns {{ apiKey: string; baseUrl: string; agentName: string } | null}
|
|
115
187
|
*/
|
|
116
188
|
function loadConfig() {
|
|
117
189
|
try {
|
|
118
|
-
const configPath =
|
|
190
|
+
const configPath = multicornConfigPath();
|
|
119
191
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
120
192
|
const obj = JSON.parse(raw);
|
|
121
193
|
const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
|
|
@@ -564,16 +636,13 @@ async function main() {
|
|
|
564
636
|
try {
|
|
565
637
|
raw = await readStdin();
|
|
566
638
|
} catch (e) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
process.exit(0);
|
|
575
|
-
}
|
|
576
|
-
if (config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
639
|
+
if (shouldEnforceGovernance()) {
|
|
640
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
641
|
+
blockMisconfigured(
|
|
642
|
+
`could not read stdin (${msg}), cannot verify permissions.`,
|
|
643
|
+
"Retry the action. If it keeps failing, check Windsurf hook configuration.",
|
|
644
|
+
);
|
|
645
|
+
}
|
|
577
646
|
process.exit(0);
|
|
578
647
|
}
|
|
579
648
|
|
|
@@ -582,8 +651,13 @@ async function main() {
|
|
|
582
651
|
try {
|
|
583
652
|
hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
584
653
|
} catch (e) {
|
|
585
|
-
|
|
586
|
-
|
|
654
|
+
if (shouldEnforceGovernance()) {
|
|
655
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
656
|
+
blockMisconfigured(
|
|
657
|
+
`invalid JSON on stdin (${msg}), cannot verify permissions.`,
|
|
658
|
+
"Retry the action or reinstall hooks with npx multicorn-shield init.",
|
|
659
|
+
);
|
|
660
|
+
}
|
|
587
661
|
process.exit(0);
|
|
588
662
|
}
|
|
589
663
|
|
|
@@ -606,6 +680,32 @@ async function main() {
|
|
|
606
680
|
}
|
|
607
681
|
const { service, actionType } = mapped;
|
|
608
682
|
|
|
683
|
+
const config = loadConfig();
|
|
684
|
+
if (config === null) {
|
|
685
|
+
if (shouldEnforceGovernance()) {
|
|
686
|
+
blockMisconfigured(
|
|
687
|
+
"Shield config missing or unreadable, cannot verify permissions.",
|
|
688
|
+
"Run npx multicorn-shield init and complete setup, then check ~/.multicorn/config.json.",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
if (config.apiKey.length === 0 || config.agentName.length === 0) {
|
|
694
|
+
if (shouldEnforceGovernance()) {
|
|
695
|
+
const cause =
|
|
696
|
+
config.apiKey.length === 0
|
|
697
|
+
? "API key missing from Shield config, cannot verify permissions."
|
|
698
|
+
: "Windsurf agent name missing from Shield config, cannot verify permissions.";
|
|
699
|
+
blockMisconfigured(
|
|
700
|
+
cause,
|
|
701
|
+
"Run npx multicorn-shield init, paste a valid mcs_ key, and configure the Windsurf agent.",
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
logInvocation(agentActionName, config.agentName, config.baseUrl);
|
|
708
|
+
|
|
609
709
|
let toolInfoSerialized;
|
|
610
710
|
try {
|
|
611
711
|
toolInfoSerialized =
|
|
@@ -614,10 +714,10 @@ async function main() {
|
|
|
614
714
|
: JSON.stringify(toolInfo === undefined ? null : toolInfo);
|
|
615
715
|
} catch (e) {
|
|
616
716
|
const msg = e instanceof Error ? e.message : String(e);
|
|
617
|
-
|
|
618
|
-
|
|
717
|
+
blockMisconfigured(
|
|
718
|
+
`could not serialize tool_info (${msg}), cannot verify permissions.`,
|
|
719
|
+
"Retry the action. If it keeps failing, reinstall hooks with npx multicorn-shield init.",
|
|
619
720
|
);
|
|
620
|
-
process.exit(0);
|
|
621
721
|
}
|
|
622
722
|
|
|
623
723
|
if (typeof toolInfoSerialized === "string" && toolInfoSerialized.length > 4096) {
|
|
@@ -718,6 +818,15 @@ async function main() {
|
|
|
718
818
|
process.exit(2);
|
|
719
819
|
}
|
|
720
820
|
|
|
821
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
822
|
+
const host = apiHostFromBaseUrl(config.baseUrl);
|
|
823
|
+
process.stderr.write(
|
|
824
|
+
`${LOG_PREFIX} Action blocked: API key not recognized for ${host}.\n` +
|
|
825
|
+
` Run npx multicorn-shield init and paste a valid key at the prompt.\n`,
|
|
826
|
+
);
|
|
827
|
+
process.exit(2);
|
|
828
|
+
}
|
|
829
|
+
|
|
721
830
|
const httpDetail = bodyText.length > 300 ? `${bodyText.slice(0, 300)}...` : bodyText;
|
|
722
831
|
process.stderr.write(
|
|
723
832
|
`${LOG_PREFIX} Action blocked: Shield returned HTTP ${String(statusCode)}, cannot verify permissions.\n` +
|