multicorn-shield 0.1.10 → 0.1.15

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
 
@@ -77,6 +155,42 @@ await shield.logAction({
77
155
 
78
156
  That gives you a consent screen, scoped permissions, and an audit trail.
79
157
 
158
+ ## Dashboard
159
+
160
+ Every action, approval, and permission is visible in real time at [app.multicorn.ai](https://app.multicorn.ai).
161
+
162
+ **Sign up:** [https://app.multicorn.ai](https://app.multicorn.ai)
163
+
164
+ With the dashboard you can:
165
+
166
+ - See all agents and their activity
167
+ - Approve or reject pending actions
168
+ - Configure per-agent permissions (read/write/execute per service)
169
+ - Set spending limits
170
+ - View the full audit trail with hash-chain integrity
171
+
172
+ The dashboard works with both the SDK integration and the MCP proxy. No extra setup needed.
173
+
174
+ <p align="center">
175
+ <img src="https://multicorn.ai/images/screenshots/overview-page.png" alt="Dashboard overview showing total actions, blocked count, spend, and live activity feed" width="800" />
176
+ </p>
177
+
178
+ <p align="center">
179
+ <img src="https://multicorn.ai/images/screenshots/approvals-card.png" alt="Approval card with one-tap approve/reject and permission duration options" width="800" />
180
+ </p>
181
+
182
+ <p align="center">
183
+ <img src="https://multicorn.ai/images/screenshots/activity-log-list.png" alt="Filterable activity log showing every agent action with status" width="800" />
184
+ </p>
185
+
186
+ <p align="center">
187
+ <img src="https://multicorn.ai/images/screenshots/consent-screen.png" alt="Consent screen where users grant agent permissions" width="800" />
188
+ </p>
189
+
190
+ <p align="center">
191
+ <img src="https://multicorn.ai/images/screenshots/agent-page-with-stats.png" alt="Agent detail page with action stats and budget tracking" width="800" />
192
+ </p>
193
+
80
194
  ## Built with Shield
81
195
 
82
196
  Multicorn is developed using AI coding agents. Primarily Cursor for code generation and GitHub Actions as the deployment agent. Every one of those agents runs under Shield.
@@ -506,6 +506,9 @@ function dollarsToCents(dollars) {
506
506
  // src/proxy/interceptor.ts
507
507
  var BLOCKED_ERROR_CODE = -32e3;
508
508
  var SPENDING_BLOCKED_ERROR_CODE = -32001;
509
+ var INTERNAL_ERROR_CODE = -32002;
510
+ var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
511
+ var AUTH_ERROR_CODE = -32004;
509
512
  function parseJsonRpcLine(line) {
510
513
  const trimmed = line.trim();
511
514
  if (trimmed.length === 0) return null;
@@ -550,6 +553,39 @@ function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
550
553
  }
551
554
  };
552
555
  }
556
+ function buildInternalErrorResponse(id) {
557
+ const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
558
+ return {
559
+ jsonrpc: "2.0",
560
+ id,
561
+ error: {
562
+ code: INTERNAL_ERROR_CODE,
563
+ message
564
+ }
565
+ };
566
+ }
567
+ function buildServiceUnreachableResponse(id, dashboardUrl) {
568
+ const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
569
+ return {
570
+ jsonrpc: "2.0",
571
+ id,
572
+ error: {
573
+ code: SERVICE_UNREACHABLE_ERROR_CODE,
574
+ message
575
+ }
576
+ };
577
+ }
578
+ function buildAuthErrorResponse(id) {
579
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-proxy init to reconfigure.";
580
+ return {
581
+ jsonrpc: "2.0",
582
+ id,
583
+ error: {
584
+ code: AUTH_ERROR_CODE,
585
+ message
586
+ }
587
+ };
588
+ }
553
589
  function extractServiceFromToolName(toolName) {
554
590
  const idx = toolName.indexOf("_");
555
591
  return idx === -1 ? toolName : toolName.slice(0, idx);
@@ -600,6 +636,13 @@ function deriveDashboardUrl(baseUrl) {
600
636
  return "https://app.multicorn.ai";
601
637
  }
602
638
  }
639
+ var ShieldAuthError = class _ShieldAuthError extends Error {
640
+ constructor(message) {
641
+ super(message);
642
+ this.name = "ShieldAuthError";
643
+ Object.setPrototypeOf(this, _ShieldAuthError.prototype);
644
+ }
645
+ };
603
646
  async function loadCachedScopes(agentName) {
604
647
  try {
605
648
  const raw = await readFile(SCOPES_PATH, "utf8");
@@ -643,8 +686,18 @@ async function findAgentByName(agentName, apiKey, baseUrl) {
643
686
  } catch {
644
687
  return null;
645
688
  }
646
- if (!response.ok) return null;
647
- const body = await response.json();
689
+ if (!response.ok) {
690
+ if (response.status === 401 || response.status === 403) {
691
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
692
+ }
693
+ return null;
694
+ }
695
+ let body;
696
+ try {
697
+ body = await response.json();
698
+ } catch {
699
+ return null;
700
+ }
648
701
  if (!isApiSuccessResponse(body)) return null;
649
702
  const agents = body.data;
650
703
  if (!Array.isArray(agents)) return null;
@@ -665,6 +718,11 @@ async function registerAgent(agentName, apiKey, baseUrl) {
665
718
  signal: AbortSignal.timeout(8e3)
666
719
  });
667
720
  if (!response.ok) {
721
+ if (response.status === 401 || response.status === 403) {
722
+ throw new ShieldAuthError(
723
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
724
+ );
725
+ }
668
726
  throw new Error(
669
727
  `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
670
728
  );
@@ -704,6 +762,9 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
704
762
  return scopes;
705
763
  }
706
764
  function openBrowser(url) {
765
+ if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
766
+ return;
767
+ }
707
768
  const platform = process.platform;
708
769
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
709
770
  spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
@@ -741,6 +802,9 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
741
802
  return { id: "", name: agentName, scopes: cachedScopes };
742
803
  }
743
804
  let agent = await findAgentByName(agentName, apiKey, baseUrl);
805
+ if (agent?.authInvalid) {
806
+ return agent;
807
+ }
744
808
  if (agent === null) {
745
809
  try {
746
810
  logger.info("Agent not found. Registering.", { agent: agentName });
@@ -748,6 +812,9 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
748
812
  agent = { id, name: agentName, scopes: [] };
749
813
  logger.info("Agent registered.", { agent: agentName, id });
750
814
  } catch (error) {
815
+ if (error instanceof ShieldAuthError) {
816
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
817
+ }
751
818
  const detail = error instanceof Error ? error.message : String(error);
752
819
  logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
753
820
  error: detail
@@ -812,6 +879,7 @@ function createProxyServer(config) {
812
879
  let spendingChecker = null;
813
880
  let grantedScopes = [];
814
881
  let agentId = "";
882
+ let authInvalid = false;
815
883
  let refreshTimer = null;
816
884
  let consentInProgress = false;
817
885
  const pendingLines = [];
@@ -864,40 +932,28 @@ function createProxyServer(config) {
864
932
  if (request.method !== "tools/call") return null;
865
933
  const toolParams = extractToolCallParams(request);
866
934
  if (toolParams === null) return null;
867
- const service = extractServiceFromToolName(toolParams.name);
868
- const action = extractActionFromToolName(toolParams.name);
869
- const requestedScope = { service, permissionLevel: "execute" };
870
- const validation = validateScopeAccess(grantedScopes, requestedScope);
871
- config.logger.debug("Tool call intercepted.", {
872
- tool: toolParams.name,
873
- service,
874
- allowed: validation.allowed
875
- });
876
- if (!validation.allowed) {
877
- await ensureConsent(requestedScope);
878
- const revalidation = validateScopeAccess(grantedScopes, requestedScope);
879
- if (!revalidation.allowed) {
880
- if (actionLogger !== null) {
881
- if (!config.agentName || config.agentName.trim().length === 0) {
882
- process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
883
- } else {
884
- await actionLogger.logAction({
885
- agent: config.agentName,
886
- service,
887
- actionType: action,
888
- status: "blocked"
889
- });
890
- }
891
- }
892
- const blocked = buildBlockedResponse(request.id, service, "execute", config.dashboardUrl);
935
+ try {
936
+ if (authInvalid) {
937
+ const blocked = buildAuthErrorResponse(request.id);
893
938
  return JSON.stringify(blocked);
894
939
  }
895
- }
896
- if (spendingChecker !== null) {
897
- const costCents = extractCostCents(toolParams.arguments);
898
- if (costCents > 0) {
899
- const spendResult = spendingChecker.checkSpend(costCents);
900
- if (!spendResult.allowed) {
940
+ if (agentId.length === 0) {
941
+ const blocked = buildServiceUnreachableResponse(request.id, config.dashboardUrl);
942
+ return JSON.stringify(blocked);
943
+ }
944
+ const service = extractServiceFromToolName(toolParams.name);
945
+ const action = extractActionFromToolName(toolParams.name);
946
+ const requestedScope = { service, permissionLevel: "execute" };
947
+ const validation = validateScopeAccess(grantedScopes, requestedScope);
948
+ config.logger.debug("Tool call intercepted.", {
949
+ tool: toolParams.name,
950
+ service,
951
+ allowed: validation.allowed
952
+ });
953
+ if (!validation.allowed) {
954
+ await ensureConsent(requestedScope);
955
+ const revalidation = validateScopeAccess(grantedScopes, requestedScope);
956
+ if (!revalidation.allowed) {
901
957
  if (actionLogger !== null) {
902
958
  if (!config.agentName || config.agentName.trim().length === 0) {
903
959
  process.stderr.write(
@@ -912,29 +968,60 @@ function createProxyServer(config) {
912
968
  });
913
969
  }
914
970
  }
915
- const blocked = buildSpendingBlockedResponse(
916
- request.id,
917
- spendResult.reason ?? "spending limit exceeded",
918
- config.dashboardUrl
971
+ return JSON.stringify(
972
+ buildBlockedResponse(request.id, service, "execute", config.dashboardUrl)
919
973
  );
920
- return JSON.stringify(blocked);
921
974
  }
922
- spendingChecker.recordSpend(costCents);
923
975
  }
924
- }
925
- if (actionLogger !== null) {
926
- if (!config.agentName || config.agentName.trim().length === 0) {
927
- process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
928
- } else {
929
- await actionLogger.logAction({
930
- agent: config.agentName,
931
- service,
932
- actionType: action,
933
- status: "approved"
934
- });
976
+ if (spendingChecker !== null) {
977
+ const costCents = extractCostCents(toolParams.arguments);
978
+ if (costCents > 0) {
979
+ const spendResult = spendingChecker.checkSpend(costCents);
980
+ if (!spendResult.allowed) {
981
+ if (actionLogger !== null) {
982
+ if (!config.agentName || config.agentName.trim().length === 0) {
983
+ process.stderr.write(
984
+ "[multicorn-proxy] Cannot log action: agent name not resolved\n"
985
+ );
986
+ } else {
987
+ await actionLogger.logAction({
988
+ agent: config.agentName,
989
+ service,
990
+ actionType: action,
991
+ status: "blocked"
992
+ });
993
+ }
994
+ }
995
+ const blocked = buildSpendingBlockedResponse(
996
+ request.id,
997
+ spendResult.reason ?? "spending limit exceeded",
998
+ config.dashboardUrl
999
+ );
1000
+ return JSON.stringify(blocked);
1001
+ }
1002
+ spendingChecker.recordSpend(costCents);
1003
+ }
935
1004
  }
1005
+ if (actionLogger !== null) {
1006
+ if (!config.agentName || config.agentName.trim().length === 0) {
1007
+ process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
1008
+ } else {
1009
+ await actionLogger.logAction({
1010
+ agent: config.agentName,
1011
+ service,
1012
+ actionType: action,
1013
+ status: "approved"
1014
+ });
1015
+ }
1016
+ }
1017
+ return null;
1018
+ } catch (error) {
1019
+ config.logger.error("Tool call handler error.", {
1020
+ error: error instanceof Error ? error.message : String(error)
1021
+ });
1022
+ const blocked = buildInternalErrorResponse(request.id);
1023
+ return JSON.stringify(blocked);
936
1024
  }
937
- return null;
938
1025
  }
939
1026
  async function processLine(line) {
940
1027
  const childProcess = child;
@@ -986,6 +1073,7 @@ function createProxyServer(config) {
986
1073
  );
987
1074
  agentId = agentRecord.id;
988
1075
  grantedScopes = agentRecord.scopes;
1076
+ authInvalid = agentRecord.authInvalid === true;
989
1077
  config.logger.info("Agent resolved.", {
990
1078
  agent: config.agentName,
991
1079
  id: agentId,
@@ -156,19 +156,19 @@ function handleHttpError(status, logger, retryDelaySeconds) {
156
156
  }
157
157
  if (status === 429) {
158
158
  {
159
- const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
159
+ const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
160
160
  process.stderr.write(`${rateLimitMsg}
161
161
  `);
162
162
  }
163
- return { shouldBlock: false };
163
+ return { shouldBlock: true };
164
164
  }
165
165
  if (status >= 500 && status < 600) {
166
- const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action was not checked \u2014 proceeding with fail-open.`;
166
+ const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
167
167
  process.stderr.write(`${serverErrorMsg}
168
168
  `);
169
- return { shouldBlock: false };
169
+ return { shouldBlock: true };
170
170
  }
171
- return { shouldBlock: false };
171
+ return { shouldBlock: true };
172
172
  }
173
173
  async function findAgentByName(agentName, apiKey, baseUrl, logger) {
174
174
  try {
@@ -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...
@@ -162,21 +162,21 @@ function handleHttpError(status, logger, retryDelaySeconds) {
162
162
  }
163
163
  if (status === 429) {
164
164
  {
165
- const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
165
+ const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
166
166
  logger?.warn(rateLimitMsg);
167
167
  process.stderr.write(`${rateLimitMsg}
168
168
  `);
169
169
  }
170
- return { shouldBlock: false };
170
+ return { shouldBlock: true };
171
171
  }
172
172
  if (status >= 500 && status < 600) {
173
- const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action was not checked \u2014 proceeding with fail-open.`;
173
+ const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
174
174
  logger?.warn(serverErrorMsg);
175
175
  process.stderr.write(`${serverErrorMsg}
176
176
  `);
177
- return { shouldBlock: false };
177
+ return { shouldBlock: true };
178
178
  }
179
- return { shouldBlock: false };
179
+ return { shouldBlock: true };
180
180
  }
181
181
  async function findAgentByName(agentName, apiKey, baseUrl, logger) {
182
182
  try {
@@ -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.15",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",