multicorn-shield 0.1.10 → 0.1.13
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/README.md +79 -1
- package/dist/multicorn-proxy.js +3 -0
- package/dist/openclaw-hook/handler.js +14 -0
- package/dist/openclaw-plugin/index.js +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ The permissions and control layer for AI agents. Open source.
|
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://bundlephobia.com/package/multicorn-shield)
|
|
12
12
|
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://multicorn.ai/images/demo.gif" alt="Multicorn Shield demo: agent blocked, user approves in dashboard, agent proceeds" width="800" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
13
19
|
## Why?
|
|
14
20
|
|
|
15
21
|
AI agents are getting access to your email, calendar, bank accounts, and code repositories. Today, most agents operate with no permission boundaries: they can read, write, and spend with no oversight. Multicorn Shield gives developers a single SDK to enforce what agents can do, track what they did, and let users stay in control.
|
|
@@ -48,7 +54,79 @@ That's it. Every tool call now goes through Shield's permission layer, and activ
|
|
|
48
54
|
|
|
49
55
|
See the [full MCP proxy guide](https://multicorn.ai/docs/mcp-proxy) for Claude Code, OpenClaw, and generic MCP client examples.
|
|
50
56
|
|
|
51
|
-
### Option 2:
|
|
57
|
+
### Option 2: OpenClaw Plugin (native integration)
|
|
58
|
+
|
|
59
|
+
If you're running [OpenClaw](https://openclaw.ai), Shield integrates directly as a plugin. No proxy layer, no code changes. The plugin intercepts every tool call at the infrastructure level before it executes.
|
|
60
|
+
|
|
61
|
+
**Step 1: Install and configure**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install -g multicorn-shield
|
|
65
|
+
npx multicorn-proxy init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Enter your API key when prompted. This saves your key to `~/.multicorn/config.json` and configures the OpenClaw hook environment.
|
|
69
|
+
|
|
70
|
+
**Step 2: Build the plugin**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cd $(npm root -g)/multicorn-shield
|
|
74
|
+
npm run build
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Step 3: Register with OpenClaw**
|
|
78
|
+
|
|
79
|
+
Add the plugin path to your `~/.openclaw/openclaw.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"plugins": {
|
|
84
|
+
"load": {
|
|
85
|
+
"paths": ["<npm-root>/multicorn-shield/dist/openclaw-plugin/index.js"]
|
|
86
|
+
},
|
|
87
|
+
"entries": {
|
|
88
|
+
"multicorn-shield": {
|
|
89
|
+
"enabled": true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Replace `<npm-root>` with the output of `npm root -g`.
|
|
97
|
+
|
|
98
|
+
**Step 4: Restart and verify**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
openclaw gateway restart
|
|
102
|
+
openclaw plugins list
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You should see `multicorn-shield` in the loaded plugins list.
|
|
106
|
+
|
|
107
|
+
**How it works**
|
|
108
|
+
|
|
109
|
+
1. Agent tries to use a tool (read files, run commands, send emails)
|
|
110
|
+
2. Shield intercepts via `before_tool_call` and checks permissions
|
|
111
|
+
3. First time: A consent screen opens in your browser so you can authorize the agent
|
|
112
|
+
4. Authorized actions: Proceed immediately
|
|
113
|
+
5. New or elevated actions: Blocked with a link to the dashboard where you approve or reject
|
|
114
|
+
6. Everything is logged to your Multicorn dashboard
|
|
115
|
+
|
|
116
|
+
The plugin maps OpenClaw tools to Shield permission scopes automatically:
|
|
117
|
+
|
|
118
|
+
| OpenClaw Tool | Shield Scope |
|
|
119
|
+
| ------------------- | ---------------- |
|
|
120
|
+
| read | filesystem:read |
|
|
121
|
+
| write, edit | filesystem:write |
|
|
122
|
+
| exec | terminal:execute |
|
|
123
|
+
| exec (rm, mv, sudo) | terminal:write |
|
|
124
|
+
| browser | browser:execute |
|
|
125
|
+
| message | messaging:write |
|
|
126
|
+
|
|
127
|
+
Destructive commands (rm, mv, sudo, chmod) are detected automatically and require separate write-level approval.
|
|
128
|
+
|
|
129
|
+
### Option 3: Integrate the SDK
|
|
52
130
|
|
|
53
131
|
For full control over consent screens, spending limits, and action logging, use the SDK directly in your application code.
|
|
54
132
|
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -704,6 +704,9 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
|
704
704
|
return scopes;
|
|
705
705
|
}
|
|
706
706
|
function openBrowser(url) {
|
|
707
|
+
if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
707
710
|
const platform = process.platform;
|
|
708
711
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
709
712
|
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
@@ -273,6 +273,16 @@ var POLL_INTERVAL_MS2 = 3e3;
|
|
|
273
273
|
var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
274
274
|
function deriveDashboardUrl(baseUrl) {
|
|
275
275
|
try {
|
|
276
|
+
const envBase = process.env["MULTICORN_BASE_URL"];
|
|
277
|
+
if (typeof envBase === "string" && envBase.trim().length > 0) {
|
|
278
|
+
const trimmed = envBase.trim();
|
|
279
|
+
if (trimmed.includes("localhost") || trimmed.includes("127.0.0.1")) {
|
|
280
|
+
baseUrl = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!/^https?:\/\//i.test(baseUrl) && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"))) {
|
|
284
|
+
baseUrl = `http://${baseUrl}`;
|
|
285
|
+
}
|
|
276
286
|
const url = new URL(baseUrl);
|
|
277
287
|
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
278
288
|
url.port = "5173";
|
|
@@ -301,6 +311,9 @@ function buildConsentUrl(agentName, dashboardUrl, scope) {
|
|
|
301
311
|
return `${base}/consent?${params.toString()}`;
|
|
302
312
|
}
|
|
303
313
|
function openBrowser(url) {
|
|
314
|
+
if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
304
317
|
const platform = process.platform;
|
|
305
318
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
306
319
|
try {
|
|
@@ -315,6 +328,7 @@ ${url}
|
|
|
315
328
|
}
|
|
316
329
|
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
317
330
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
331
|
+
console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
|
|
318
332
|
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
319
333
|
process.stderr.write(
|
|
320
334
|
`[multicorn-shield] Opening consent page...
|
|
@@ -335,6 +335,16 @@ var POLL_INTERVAL_MS2 = 3e3;
|
|
|
335
335
|
var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
336
336
|
function deriveDashboardUrl(baseUrl) {
|
|
337
337
|
try {
|
|
338
|
+
const envBase = process.env["MULTICORN_BASE_URL"];
|
|
339
|
+
if (typeof envBase === "string" && envBase.trim().length > 0) {
|
|
340
|
+
const trimmed = envBase.trim();
|
|
341
|
+
if (trimmed.includes("localhost") || trimmed.includes("127.0.0.1")) {
|
|
342
|
+
baseUrl = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!/^https?:\/\//i.test(baseUrl) && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1"))) {
|
|
346
|
+
baseUrl = `http://${baseUrl}`;
|
|
347
|
+
}
|
|
338
348
|
const url = new URL(baseUrl);
|
|
339
349
|
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
340
350
|
url.port = "5173";
|
|
@@ -363,6 +373,9 @@ function buildConsentUrl(agentName, dashboardUrl, scope) {
|
|
|
363
373
|
return `${base}/consent?${params.toString()}`;
|
|
364
374
|
}
|
|
365
375
|
function openBrowser(url) {
|
|
376
|
+
if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
366
379
|
const platform = process.platform;
|
|
367
380
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
368
381
|
try {
|
|
@@ -377,6 +390,7 @@ ${url}
|
|
|
377
390
|
}
|
|
378
391
|
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
379
392
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
393
|
+
console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
|
|
380
394
|
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
381
395
|
process.stderr.write(
|
|
382
396
|
`[multicorn-shield] Opening consent page...
|
|
@@ -703,6 +717,46 @@ async function beforeToolCall(event, ctx) {
|
|
|
703
717
|
if (!hasScope(grantedScopes, requestedScope) && agentRecord !== null) {
|
|
704
718
|
await ensureConsent(agentName, config.apiKey, config.baseUrl, mapping);
|
|
705
719
|
console.error("[SHIELD] ensureConsent result: completed (blocked path)");
|
|
720
|
+
const scopes = await fetchGrantedScopes(
|
|
721
|
+
agentRecord.id,
|
|
722
|
+
config.apiKey,
|
|
723
|
+
config.baseUrl,
|
|
724
|
+
pluginLogger ?? void 0
|
|
725
|
+
);
|
|
726
|
+
grantedScopes = scopes;
|
|
727
|
+
lastScopeRefresh = Date.now();
|
|
728
|
+
if (Array.isArray(scopes) && scopes.length > 0) {
|
|
729
|
+
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const recheckResult = await checkActionPermission(
|
|
733
|
+
{
|
|
734
|
+
agent: agentName,
|
|
735
|
+
service: mapping.service,
|
|
736
|
+
actionType,
|
|
737
|
+
status: "approved",
|
|
738
|
+
metadata: { description }
|
|
739
|
+
},
|
|
740
|
+
config.apiKey,
|
|
741
|
+
config.baseUrl,
|
|
742
|
+
pluginLogger ?? void 0
|
|
743
|
+
);
|
|
744
|
+
if (recheckResult.status === "approved") {
|
|
745
|
+
const refreshedScopes = await fetchGrantedScopes(
|
|
746
|
+
agentRecord.id,
|
|
747
|
+
config.apiKey,
|
|
748
|
+
config.baseUrl,
|
|
749
|
+
pluginLogger ?? void 0
|
|
750
|
+
);
|
|
751
|
+
grantedScopes = refreshedScopes;
|
|
752
|
+
lastScopeRefresh = Date.now();
|
|
753
|
+
if (Array.isArray(refreshedScopes) && refreshedScopes.length > 0) {
|
|
754
|
+
await saveCachedScopes(agentName, agentRecord.id, refreshedScopes).catch(() => {
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
console.error("[SHIELD] DECISION: allow (re-check after consent)");
|
|
758
|
+
return void 0;
|
|
759
|
+
}
|
|
706
760
|
}
|
|
707
761
|
const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
|
|
708
762
|
const dashboardUrl = deriveDashboardUrl(config.baseUrl);
|