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 CHANGED
@@ -10,6 +10,12 @@ The permissions and control layer for AI agents. Open source.
10
10
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
11
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/multicorn-shield)](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: Integrate the SDK
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
 
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",