multicorn-shield 0.1.13 → 0.1.16

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
@@ -155,6 +155,42 @@ await shield.logAction({
155
155
 
156
156
  That gives you a consent screen, scoped permissions, and an audit trail.
157
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
+
158
194
  ## Built with Shield
159
195
 
160
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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, mkdir, writeFile } from 'fs/promises';
2
+ import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { createInterface } from 'readline';
6
6
  import { spawn } from 'child_process';
7
+ import { createHash } from 'crypto';
7
8
  import 'stream';
8
9
 
9
10
  var CONFIG_DIR = join(homedir(), ".multicorn");
@@ -506,6 +507,9 @@ function dollarsToCents(dollars) {
506
507
  // src/proxy/interceptor.ts
507
508
  var BLOCKED_ERROR_CODE = -32e3;
508
509
  var SPENDING_BLOCKED_ERROR_CODE = -32001;
510
+ var INTERNAL_ERROR_CODE = -32002;
511
+ var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
512
+ var AUTH_ERROR_CODE = -32004;
509
513
  function parseJsonRpcLine(line) {
510
514
  const trimmed = line.trim();
511
515
  if (trimmed.length === 0) return null;
@@ -550,6 +554,39 @@ function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
550
554
  }
551
555
  };
552
556
  }
557
+ function buildInternalErrorResponse(id) {
558
+ const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
559
+ return {
560
+ jsonrpc: "2.0",
561
+ id,
562
+ error: {
563
+ code: INTERNAL_ERROR_CODE,
564
+ message
565
+ }
566
+ };
567
+ }
568
+ function buildServiceUnreachableResponse(id, dashboardUrl) {
569
+ const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
570
+ return {
571
+ jsonrpc: "2.0",
572
+ id,
573
+ error: {
574
+ code: SERVICE_UNREACHABLE_ERROR_CODE,
575
+ message
576
+ }
577
+ };
578
+ }
579
+ function buildAuthErrorResponse(id) {
580
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-proxy init to reconfigure.";
581
+ return {
582
+ jsonrpc: "2.0",
583
+ id,
584
+ error: {
585
+ code: AUTH_ERROR_CODE,
586
+ message
587
+ }
588
+ };
589
+ }
553
590
  function extractServiceFromToolName(toolName) {
554
591
  const idx = toolName.indexOf("_");
555
592
  return idx === -1 ? toolName : toolName.slice(0, idx);
@@ -574,44 +611,53 @@ function capitalize(str) {
574
611
  }
575
612
  var MULTICORN_DIR = join(homedir(), ".multicorn");
576
613
  var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
577
- var CONSENT_POLL_INTERVAL_MS = 3e3;
578
- var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
579
- function deriveDashboardUrl(baseUrl) {
614
+ var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
615
+ function cacheKey(agentName, apiKey) {
616
+ return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
617
+ }
618
+ async function ensureCacheIdentity(apiKey) {
619
+ const currentHash = createHash("sha256").update(apiKey).digest("hex");
620
+ let storedHash = null;
580
621
  try {
581
- const url = new URL(baseUrl);
582
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
583
- url.port = "5173";
584
- url.protocol = "http:";
585
- return url.toString();
586
- }
587
- if (url.hostname === "api.multicorn.ai") {
588
- url.hostname = "app.multicorn.ai";
589
- return url.toString();
590
- }
591
- if (url.hostname.includes("api")) {
592
- url.hostname = url.hostname.replace("api", "app");
593
- return url.toString();
594
- }
595
- if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
596
- return "https://app.multicorn.ai";
622
+ const raw = await readFile(CACHE_META_PATH, "utf8");
623
+ const meta = JSON.parse(raw);
624
+ if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
625
+ storedHash = meta.apiKeyHash;
597
626
  }
598
- return "https://app.multicorn.ai";
599
627
  } catch {
600
- return "https://app.multicorn.ai";
628
+ }
629
+ if (storedHash === null || storedHash !== currentHash) {
630
+ try {
631
+ await unlink(SCOPES_PATH);
632
+ } catch {
633
+ }
634
+ }
635
+ if (storedHash !== currentHash) {
636
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
637
+ await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
638
+ encoding: "utf8",
639
+ mode: 384
640
+ });
601
641
  }
602
642
  }
603
- async function loadCachedScopes(agentName) {
643
+ async function loadCachedScopes(agentName, apiKey) {
644
+ if (apiKey.length === 0) return null;
645
+ await ensureCacheIdentity(apiKey);
646
+ const key = cacheKey(agentName, apiKey);
604
647
  try {
605
648
  const raw = await readFile(SCOPES_PATH, "utf8");
606
649
  const parsed = JSON.parse(raw);
607
650
  if (!isScopesCacheFile(parsed)) return null;
608
- const entry = parsed[agentName];
651
+ const entry = parsed[key];
609
652
  return entry?.scopes ?? null;
610
653
  } catch {
611
654
  return null;
612
655
  }
613
656
  }
614
- async function saveCachedScopes(agentName, agentId, scopes) {
657
+ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
658
+ if (apiKey.length === 0) return;
659
+ await ensureCacheIdentity(apiKey);
660
+ const key = cacheKey(agentName, apiKey);
615
661
  await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
616
662
  let existing = {};
617
663
  try {
@@ -622,7 +668,7 @@ async function saveCachedScopes(agentName, agentId, scopes) {
622
668
  }
623
669
  const updated = {
624
670
  ...existing,
625
- [agentName]: {
671
+ [key]: {
626
672
  agentId,
627
673
  scopes,
628
674
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -633,6 +679,44 @@ async function saveCachedScopes(agentName, agentId, scopes) {
633
679
  mode: 384
634
680
  });
635
681
  }
682
+ function isScopesCacheFile(value) {
683
+ return typeof value === "object" && value !== null;
684
+ }
685
+
686
+ // src/proxy/consent.ts
687
+ var CONSENT_POLL_INTERVAL_MS = 3e3;
688
+ var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
689
+ function deriveDashboardUrl(baseUrl) {
690
+ try {
691
+ const url = new URL(baseUrl);
692
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
693
+ url.port = "5173";
694
+ url.protocol = "http:";
695
+ return url.toString();
696
+ }
697
+ if (url.hostname === "api.multicorn.ai") {
698
+ url.hostname = "app.multicorn.ai";
699
+ return url.toString();
700
+ }
701
+ if (url.hostname.includes("api")) {
702
+ url.hostname = url.hostname.replace("api", "app");
703
+ return url.toString();
704
+ }
705
+ if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
706
+ return "https://app.multicorn.ai";
707
+ }
708
+ return "https://app.multicorn.ai";
709
+ } catch {
710
+ return "https://app.multicorn.ai";
711
+ }
712
+ }
713
+ var ShieldAuthError = class _ShieldAuthError extends Error {
714
+ constructor(message) {
715
+ super(message);
716
+ this.name = "ShieldAuthError";
717
+ Object.setPrototypeOf(this, _ShieldAuthError.prototype);
718
+ }
719
+ };
636
720
  async function findAgentByName(agentName, apiKey, baseUrl) {
637
721
  let response;
638
722
  try {
@@ -643,8 +727,18 @@ async function findAgentByName(agentName, apiKey, baseUrl) {
643
727
  } catch {
644
728
  return null;
645
729
  }
646
- if (!response.ok) return null;
647
- const body = await response.json();
730
+ if (!response.ok) {
731
+ if (response.status === 401 || response.status === 403) {
732
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
733
+ }
734
+ return null;
735
+ }
736
+ let body;
737
+ try {
738
+ body = await response.json();
739
+ } catch {
740
+ return null;
741
+ }
648
742
  if (!isApiSuccessResponse(body)) return null;
649
743
  const agents = body.data;
650
744
  if (!Array.isArray(agents)) return null;
@@ -665,6 +759,11 @@ async function registerAgent(agentName, apiKey, baseUrl) {
665
759
  signal: AbortSignal.timeout(8e3)
666
760
  });
667
761
  if (!response.ok) {
762
+ if (response.status === 401 || response.status === 403) {
763
+ throw new ShieldAuthError(
764
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
765
+ );
766
+ }
668
767
  throw new Error(
669
768
  `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
670
769
  );
@@ -738,12 +837,15 @@ Waiting for you to grant access in the Multicorn dashboard...
738
837
  );
739
838
  }
740
839
  async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
741
- const cachedScopes = await loadCachedScopes(agentName);
840
+ const cachedScopes = await loadCachedScopes(agentName, apiKey);
742
841
  if (cachedScopes !== null && cachedScopes.length > 0) {
743
842
  logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
744
843
  return { id: "", name: agentName, scopes: cachedScopes };
745
844
  }
746
845
  let agent = await findAgentByName(agentName, apiKey, baseUrl);
846
+ if (agent?.authInvalid) {
847
+ return agent;
848
+ }
747
849
  if (agent === null) {
748
850
  try {
749
851
  logger.info("Agent not found. Registering.", { agent: agentName });
@@ -751,6 +853,9 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
751
853
  agent = { id, name: agentName, scopes: [] };
752
854
  logger.info("Agent registered.", { agent: agentName, id });
753
855
  } catch (error) {
856
+ if (error instanceof ShieldAuthError) {
857
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
858
+ }
754
859
  const detail = error instanceof Error ? error.message : String(error);
755
860
  logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
756
861
  error: detail
@@ -760,7 +865,7 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
760
865
  }
761
866
  const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
762
867
  if (scopes.length > 0) {
763
- await saveCachedScopes(agentName, agent.id, scopes);
868
+ await saveCachedScopes(agentName, agent.id, scopes, apiKey);
764
869
  }
765
870
  return { ...agent, scopes };
766
871
  }
@@ -798,9 +903,6 @@ function isPermissionShape(value) {
798
903
  const obj = value;
799
904
  return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || typeof obj["revoked_at"] === "string");
800
905
  }
801
- function isScopesCacheFile(value) {
802
- return typeof value === "object" && value !== null;
803
- }
804
906
 
805
907
  // src/proxy/index.ts
806
908
  var DEFAULT_SCOPE_REFRESH_INTERVAL_MS = 6e4;
@@ -815,6 +917,7 @@ function createProxyServer(config) {
815
917
  let spendingChecker = null;
816
918
  let grantedScopes = [];
817
919
  let agentId = "";
920
+ let authInvalid = false;
818
921
  let refreshTimer = null;
819
922
  let consentInProgress = false;
820
923
  const pendingLines = [];
@@ -827,7 +930,7 @@ function createProxyServer(config) {
827
930
  const scopes = await fetchGrantedScopes(agentId, config.apiKey, config.baseUrl);
828
931
  grantedScopes = scopes;
829
932
  if (scopes.length > 0) {
830
- await saveCachedScopes(config.agentName, agentId, scopes);
933
+ await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
831
934
  }
832
935
  config.logger.debug("Scopes refreshed.", { count: scopes.length });
833
936
  } catch (error) {
@@ -856,7 +959,7 @@ function createProxyServer(config) {
856
959
  scopeParam
857
960
  );
858
961
  grantedScopes = scopes;
859
- await saveCachedScopes(config.agentName, agentId, scopes);
962
+ await saveCachedScopes(config.agentName, agentId, scopes, config.apiKey);
860
963
  } finally {
861
964
  consentInProgress = false;
862
965
  }
@@ -867,40 +970,28 @@ function createProxyServer(config) {
867
970
  if (request.method !== "tools/call") return null;
868
971
  const toolParams = extractToolCallParams(request);
869
972
  if (toolParams === null) return null;
870
- const service = extractServiceFromToolName(toolParams.name);
871
- const action = extractActionFromToolName(toolParams.name);
872
- const requestedScope = { service, permissionLevel: "execute" };
873
- const validation = validateScopeAccess(grantedScopes, requestedScope);
874
- config.logger.debug("Tool call intercepted.", {
875
- tool: toolParams.name,
876
- service,
877
- allowed: validation.allowed
878
- });
879
- if (!validation.allowed) {
880
- await ensureConsent(requestedScope);
881
- const revalidation = validateScopeAccess(grantedScopes, requestedScope);
882
- if (!revalidation.allowed) {
883
- if (actionLogger !== null) {
884
- if (!config.agentName || config.agentName.trim().length === 0) {
885
- process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
886
- } else {
887
- await actionLogger.logAction({
888
- agent: config.agentName,
889
- service,
890
- actionType: action,
891
- status: "blocked"
892
- });
893
- }
894
- }
895
- const blocked = buildBlockedResponse(request.id, service, "execute", config.dashboardUrl);
973
+ try {
974
+ if (authInvalid) {
975
+ const blocked = buildAuthErrorResponse(request.id);
896
976
  return JSON.stringify(blocked);
897
977
  }
898
- }
899
- if (spendingChecker !== null) {
900
- const costCents = extractCostCents(toolParams.arguments);
901
- if (costCents > 0) {
902
- const spendResult = spendingChecker.checkSpend(costCents);
903
- if (!spendResult.allowed) {
978
+ if (agentId.length === 0) {
979
+ const blocked = buildServiceUnreachableResponse(request.id, config.dashboardUrl);
980
+ return JSON.stringify(blocked);
981
+ }
982
+ const service = extractServiceFromToolName(toolParams.name);
983
+ const action = extractActionFromToolName(toolParams.name);
984
+ const requestedScope = { service, permissionLevel: "execute" };
985
+ const validation = validateScopeAccess(grantedScopes, requestedScope);
986
+ config.logger.debug("Tool call intercepted.", {
987
+ tool: toolParams.name,
988
+ service,
989
+ allowed: validation.allowed
990
+ });
991
+ if (!validation.allowed) {
992
+ await ensureConsent(requestedScope);
993
+ const revalidation = validateScopeAccess(grantedScopes, requestedScope);
994
+ if (!revalidation.allowed) {
904
995
  if (actionLogger !== null) {
905
996
  if (!config.agentName || config.agentName.trim().length === 0) {
906
997
  process.stderr.write(
@@ -915,29 +1006,60 @@ function createProxyServer(config) {
915
1006
  });
916
1007
  }
917
1008
  }
918
- const blocked = buildSpendingBlockedResponse(
919
- request.id,
920
- spendResult.reason ?? "spending limit exceeded",
921
- config.dashboardUrl
1009
+ return JSON.stringify(
1010
+ buildBlockedResponse(request.id, service, "execute", config.dashboardUrl)
922
1011
  );
923
- return JSON.stringify(blocked);
924
1012
  }
925
- spendingChecker.recordSpend(costCents);
926
1013
  }
927
- }
928
- if (actionLogger !== null) {
929
- if (!config.agentName || config.agentName.trim().length === 0) {
930
- process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
931
- } else {
932
- await actionLogger.logAction({
933
- agent: config.agentName,
934
- service,
935
- actionType: action,
936
- status: "approved"
937
- });
1014
+ if (spendingChecker !== null) {
1015
+ const costCents = extractCostCents(toolParams.arguments);
1016
+ if (costCents > 0) {
1017
+ const spendResult = spendingChecker.checkSpend(costCents);
1018
+ if (!spendResult.allowed) {
1019
+ if (actionLogger !== null) {
1020
+ if (!config.agentName || config.agentName.trim().length === 0) {
1021
+ process.stderr.write(
1022
+ "[multicorn-proxy] Cannot log action: agent name not resolved\n"
1023
+ );
1024
+ } else {
1025
+ await actionLogger.logAction({
1026
+ agent: config.agentName,
1027
+ service,
1028
+ actionType: action,
1029
+ status: "blocked"
1030
+ });
1031
+ }
1032
+ }
1033
+ const blocked = buildSpendingBlockedResponse(
1034
+ request.id,
1035
+ spendResult.reason ?? "spending limit exceeded",
1036
+ config.dashboardUrl
1037
+ );
1038
+ return JSON.stringify(blocked);
1039
+ }
1040
+ spendingChecker.recordSpend(costCents);
1041
+ }
938
1042
  }
1043
+ if (actionLogger !== null) {
1044
+ if (!config.agentName || config.agentName.trim().length === 0) {
1045
+ process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
1046
+ } else {
1047
+ await actionLogger.logAction({
1048
+ agent: config.agentName,
1049
+ service,
1050
+ actionType: action,
1051
+ status: "approved"
1052
+ });
1053
+ }
1054
+ }
1055
+ return null;
1056
+ } catch (error) {
1057
+ config.logger.error("Tool call handler error.", {
1058
+ error: error instanceof Error ? error.message : String(error)
1059
+ });
1060
+ const blocked = buildInternalErrorResponse(request.id);
1061
+ return JSON.stringify(blocked);
939
1062
  }
940
- return null;
941
1063
  }
942
1064
  async function processLine(line) {
943
1065
  const childProcess = child;
@@ -989,6 +1111,7 @@ function createProxyServer(config) {
989
1111
  );
990
1112
  agentId = agentRecord.id;
991
1113
  grantedScopes = agentRecord.scopes;
1114
+ authInvalid = agentRecord.authInvalid === true;
992
1115
  config.logger.info("Agent resolved.", {
993
1116
  agent: config.agentName,
994
1117
  id: agentId,
@@ -1,4 +1,5 @@
1
- import { readFile, mkdir, writeFile } from 'fs/promises';
1
+ import { createHash } from 'crypto';
2
+ import { readFile, mkdir, writeFile, unlink } from 'fs/promises';
2
3
  import { join } from 'path';
3
4
  import { homedir } from 'os';
4
5
  import { spawn } from 'child_process';
@@ -83,18 +84,53 @@ function mapToolToScope(toolName, command) {
83
84
  }
84
85
  var MULTICORN_DIR = join(homedir(), ".multicorn");
85
86
  var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
86
- async function loadCachedScopes(agentName) {
87
+ var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
88
+ function cacheKey(agentName, apiKey) {
89
+ return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
90
+ }
91
+ async function ensureCacheIdentity(apiKey) {
92
+ const currentHash = createHash("sha256").update(apiKey).digest("hex");
93
+ let storedHash = null;
94
+ try {
95
+ const raw = await readFile(CACHE_META_PATH, "utf8");
96
+ const meta = JSON.parse(raw);
97
+ if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
98
+ storedHash = meta.apiKeyHash;
99
+ }
100
+ } catch {
101
+ }
102
+ if (storedHash === null || storedHash !== currentHash) {
103
+ try {
104
+ await unlink(SCOPES_PATH);
105
+ } catch {
106
+ }
107
+ }
108
+ if (storedHash !== currentHash) {
109
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
110
+ await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
111
+ encoding: "utf8",
112
+ mode: 384
113
+ });
114
+ }
115
+ }
116
+ async function loadCachedScopes(agentName, apiKey) {
117
+ if (apiKey.length === 0) return null;
118
+ await ensureCacheIdentity(apiKey);
119
+ const key = cacheKey(agentName, apiKey);
87
120
  try {
88
121
  const raw = await readFile(SCOPES_PATH, "utf8");
89
122
  const parsed = JSON.parse(raw);
90
123
  if (!isScopesCacheFile(parsed)) return null;
91
- const entry = parsed[agentName];
124
+ const entry = parsed[key];
92
125
  return entry?.scopes ?? null;
93
126
  } catch {
94
127
  return null;
95
128
  }
96
129
  }
97
- async function saveCachedScopes(agentName, agentId, scopes) {
130
+ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
131
+ if (apiKey.length === 0) return;
132
+ await ensureCacheIdentity(apiKey);
133
+ const key = cacheKey(agentName, apiKey);
98
134
  await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
99
135
  let existing = {};
100
136
  try {
@@ -105,7 +141,7 @@ async function saveCachedScopes(agentName, agentId, scopes) {
105
141
  }
106
142
  const updated = {
107
143
  ...existing,
108
- [agentName]: {
144
+ [key]: {
109
145
  agentId,
110
146
  scopes,
111
147
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -156,19 +192,19 @@ function handleHttpError(status, logger, retryDelaySeconds) {
156
192
  }
157
193
  if (status === 429) {
158
194
  {
159
- const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
195
+ const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
160
196
  process.stderr.write(`${rateLimitMsg}
161
197
  `);
162
198
  }
163
- return { shouldBlock: false };
199
+ return { shouldBlock: true };
164
200
  }
165
201
  if (status >= 500 && status < 600) {
166
- const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action was not checked \u2014 proceeding with fail-open.`;
202
+ const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
167
203
  process.stderr.write(`${serverErrorMsg}
168
204
  `);
169
- return { shouldBlock: false };
205
+ return { shouldBlock: true };
170
206
  }
171
- return { shouldBlock: false };
207
+ return { shouldBlock: true };
172
208
  }
173
209
  async function findAgentByName(agentName, apiKey, baseUrl, logger) {
174
210
  try {
@@ -201,6 +237,13 @@ async function registerAgent(agentName, apiKey, baseUrl, logger) {
201
237
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
202
238
  });
203
239
  if (!response.ok) {
240
+ const body2 = await response.json().catch(() => null);
241
+ if (response.status === 403) {
242
+ const msg = (body2?.error?.message ?? "").toLowerCase();
243
+ if (msg.includes("agent limit") || msg.includes("maximum")) {
244
+ throw new Error("Agent limit reached. Upgrade your plan at app.multicorn.ai/settings.");
245
+ }
246
+ }
204
247
  handleHttpError(response.status);
205
248
  throw new Error(
206
249
  `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
@@ -212,15 +255,28 @@ async function registerAgent(agentName, apiKey, baseUrl, logger) {
212
255
  }
213
256
  return body.data.id;
214
257
  }
258
+ var findOrRegisterInflight = /* @__PURE__ */ new Map();
215
259
  async function findOrRegisterAgent(agentName, apiKey, baseUrl, logger) {
216
- const existing = await findAgentByName(agentName, apiKey, baseUrl, logger);
217
- if (existing !== null) return existing;
218
- try {
219
- const id = await registerAgent(agentName, apiKey, baseUrl, logger);
220
- return { id, name: agentName };
221
- } catch {
222
- return null;
223
- }
260
+ const key = `${agentName}:${apiKey}:${baseUrl}`;
261
+ const existing = findOrRegisterInflight.get(key);
262
+ if (existing !== void 0) return existing;
263
+ const promise = (async () => {
264
+ const found = await findAgentByName(agentName, apiKey, baseUrl, logger);
265
+ if (found !== null) return found;
266
+ try {
267
+ const id = await registerAgent(agentName, apiKey, baseUrl, logger);
268
+ return { id, name: agentName };
269
+ } catch (err) {
270
+ if (err instanceof Error && err.message.includes("Agent limit reached")) {
271
+ throw err;
272
+ }
273
+ return null;
274
+ }
275
+ })().finally(() => {
276
+ findOrRegisterInflight.delete(key);
277
+ });
278
+ findOrRegisterInflight.set(key, promise);
279
+ return promise;
224
280
  }
225
281
  async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
226
282
  try {
@@ -366,6 +422,7 @@ var agentRecord = null;
366
422
  var grantedScopes = [];
367
423
  var consentInProgress = false;
368
424
  var lastScopeRefresh = 0;
425
+ var pinnedAgentName = null;
369
426
  var SCOPE_REFRESH_INTERVAL_MS = 6e4;
370
427
  function readConfig() {
371
428
  const apiKey = process.env["MULTICORN_API_KEY"] ?? "";
@@ -386,12 +443,20 @@ function resolveAgentName(sessionKey, envOverride) {
386
443
  }
387
444
  return "openclaw";
388
445
  }
446
+ function getAgentName(sessionKey, envOverride) {
447
+ if (pinnedAgentName !== null) return pinnedAgentName;
448
+ const resolved = resolveAgentName(sessionKey, envOverride);
449
+ if (resolved !== "openclaw") {
450
+ pinnedAgentName = resolved;
451
+ }
452
+ return resolved;
453
+ }
389
454
  async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
390
455
  if (agentRecord !== null && Date.now() - lastScopeRefresh < SCOPE_REFRESH_INTERVAL_MS) {
391
456
  return "ready";
392
457
  }
393
458
  if (agentRecord === null) {
394
- const cached = await loadCachedScopes(agentName);
459
+ const cached = await loadCachedScopes(agentName, apiKey);
395
460
  if (cached !== null && cached.length > 0) {
396
461
  grantedScopes = cached;
397
462
  void findOrRegisterAgent(agentName, apiKey, baseUrl).then((record) => {
@@ -418,7 +483,7 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
418
483
  grantedScopes = scopes;
419
484
  lastScopeRefresh = Date.now();
420
485
  if (scopes.length > 0) {
421
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
486
+ await saveCachedScopes(agentName, agentRecord.id, scopes, apiKey).catch(() => {
422
487
  });
423
488
  }
424
489
  return "ready";
@@ -444,7 +509,7 @@ async function ensureConsent(agentName, apiKey, baseUrl, scope) {
444
509
  try {
445
510
  const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl, scope);
446
511
  grantedScopes = scopes;
447
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
512
+ await saveCachedScopes(agentName, agentRecord.id, scopes, apiKey).catch(() => {
448
513
  });
449
514
  } finally {
450
515
  consentInProgress = false;
@@ -466,7 +531,10 @@ var handler = async (event) => {
466
531
  );
467
532
  return;
468
533
  }
469
- const agentName = resolveAgentName(event.sessionKey, config.agentName);
534
+ if (config.agentName !== null) {
535
+ pinnedAgentName = config.agentName;
536
+ }
537
+ const agentName = getAgentName(event.sessionKey, config.agentName);
470
538
  const readiness = await ensureAgent(agentName, config.apiKey, config.baseUrl, config.failMode);
471
539
  if (readiness === "block") {
472
540
  event.messages.push(
@@ -488,9 +556,10 @@ var handler = async (event) => {
488
556
  const permitted = isPermitted(event);
489
557
  if (!permitted) {
490
558
  const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
491
- const dashboardUrl = deriveDashboardUrl(config.baseUrl);
559
+ const base = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
492
560
  event.messages.push(
493
- `Permission denied: ${capitalizedService} ${mapping.permissionLevel} access is not allowed. Check pending approvals at ${dashboardUrl}/approvals `
561
+ `Permission denied: ${capitalizedService} ${mapping.permissionLevel} access is not allowed. Check pending approvals at:
562
+ ${base}/approvals`
494
563
  );
495
564
  void logAction(
496
565
  {
@@ -520,6 +589,7 @@ function resetState() {
520
589
  grantedScopes = [];
521
590
  consentInProgress = false;
522
591
  lastScopeRefresh = 0;
592
+ pinnedAgentName = null;
523
593
  }
524
594
 
525
595
  export { handler, readConfig, resetState, resolveAgentName };
@@ -3,7 +3,8 @@ import * as path from 'path';
3
3
  import { join } from 'path';
4
4
  import * as os from 'os';
5
5
  import { homedir } from 'os';
6
- import { mkdir, readFile, writeFile } from 'fs/promises';
6
+ import { createHash } from 'crypto';
7
+ import { mkdir, readFile, writeFile, unlink } from 'fs/promises';
7
8
  import { spawn } from 'child_process';
8
9
 
9
10
  // Multicorn Shield plugin for OpenClaw - https://multicorn.ai
@@ -88,18 +89,53 @@ function mapToolToScope(toolName, command) {
88
89
  }
89
90
  var MULTICORN_DIR = join(homedir(), ".multicorn");
90
91
  var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
91
- async function loadCachedScopes(agentName) {
92
+ var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
93
+ function cacheKey(agentName, apiKey) {
94
+ return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
95
+ }
96
+ async function ensureCacheIdentity(apiKey) {
97
+ const currentHash = createHash("sha256").update(apiKey).digest("hex");
98
+ let storedHash = null;
99
+ try {
100
+ const raw = await readFile(CACHE_META_PATH, "utf8");
101
+ const meta = JSON.parse(raw);
102
+ if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
103
+ storedHash = meta.apiKeyHash;
104
+ }
105
+ } catch {
106
+ }
107
+ if (storedHash === null || storedHash !== currentHash) {
108
+ try {
109
+ await unlink(SCOPES_PATH);
110
+ } catch {
111
+ }
112
+ }
113
+ if (storedHash !== currentHash) {
114
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
115
+ await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
116
+ encoding: "utf8",
117
+ mode: 384
118
+ });
119
+ }
120
+ }
121
+ async function loadCachedScopes(agentName, apiKey) {
122
+ if (apiKey.length === 0) return null;
123
+ await ensureCacheIdentity(apiKey);
124
+ const key = cacheKey(agentName, apiKey);
92
125
  try {
93
126
  const raw = await readFile(SCOPES_PATH, "utf8");
94
127
  const parsed = JSON.parse(raw);
95
128
  if (!isScopesCacheFile(parsed)) return null;
96
- const entry = parsed[agentName];
129
+ const entry = parsed[key];
97
130
  return entry?.scopes ?? null;
98
131
  } catch {
99
132
  return null;
100
133
  }
101
134
  }
102
- async function saveCachedScopes(agentName, agentId, scopes) {
135
+ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
136
+ if (apiKey.length === 0) return;
137
+ await ensureCacheIdentity(apiKey);
138
+ const key = cacheKey(agentName, apiKey);
103
139
  await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
104
140
  let existing = {};
105
141
  try {
@@ -110,7 +146,7 @@ async function saveCachedScopes(agentName, agentId, scopes) {
110
146
  }
111
147
  const updated = {
112
148
  ...existing,
113
- [agentName]: {
149
+ [key]: {
114
150
  agentId,
115
151
  scopes,
116
152
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -162,21 +198,21 @@ function handleHttpError(status, logger, retryDelaySeconds) {
162
198
  }
163
199
  if (status === 429) {
164
200
  {
165
- const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
201
+ const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action blocked: Shield cannot verify permissions.";
166
202
  logger?.warn(rateLimitMsg);
167
203
  process.stderr.write(`${rateLimitMsg}
168
204
  `);
169
205
  }
170
- return { shouldBlock: false };
206
+ return { shouldBlock: true };
171
207
  }
172
208
  if (status >= 500 && status < 600) {
173
- const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action was not checked \u2014 proceeding with fail-open.`;
209
+ const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action blocked: Shield cannot verify permissions.`;
174
210
  logger?.warn(serverErrorMsg);
175
211
  process.stderr.write(`${serverErrorMsg}
176
212
  `);
177
- return { shouldBlock: false };
213
+ return { shouldBlock: true };
178
214
  }
179
- return { shouldBlock: false };
215
+ return { shouldBlock: true };
180
216
  }
181
217
  async function findAgentByName(agentName, apiKey, baseUrl, logger) {
182
218
  try {
@@ -209,6 +245,13 @@ async function registerAgent(agentName, apiKey, baseUrl, logger) {
209
245
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
210
246
  });
211
247
  if (!response.ok) {
248
+ const body2 = await response.json().catch(() => null);
249
+ if (response.status === 403) {
250
+ const msg = (body2?.error?.message ?? "").toLowerCase();
251
+ if (msg.includes("agent limit") || msg.includes("maximum")) {
252
+ throw new Error("Agent limit reached. Upgrade your plan at app.multicorn.ai/settings.");
253
+ }
254
+ }
212
255
  handleHttpError(response.status, logger);
213
256
  throw new Error(
214
257
  `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
@@ -220,15 +263,28 @@ async function registerAgent(agentName, apiKey, baseUrl, logger) {
220
263
  }
221
264
  return body.data.id;
222
265
  }
266
+ var findOrRegisterInflight = /* @__PURE__ */ new Map();
223
267
  async function findOrRegisterAgent(agentName, apiKey, baseUrl, logger) {
224
- const existing = await findAgentByName(agentName, apiKey, baseUrl, logger);
225
- if (existing !== null) return existing;
226
- try {
227
- const id = await registerAgent(agentName, apiKey, baseUrl, logger);
228
- return { id, name: agentName };
229
- } catch {
230
- return null;
231
- }
268
+ const key = `${agentName}:${apiKey}:${baseUrl}`;
269
+ const existing = findOrRegisterInflight.get(key);
270
+ if (existing !== void 0) return existing;
271
+ const promise = (async () => {
272
+ const found = await findAgentByName(agentName, apiKey, baseUrl, logger);
273
+ if (found !== null) return found;
274
+ try {
275
+ const id = await registerAgent(agentName, apiKey, baseUrl, logger);
276
+ return { id, name: agentName };
277
+ } catch (err) {
278
+ if (err instanceof Error && err.message.includes("Agent limit reached")) {
279
+ throw err;
280
+ }
281
+ return null;
282
+ }
283
+ })().finally(() => {
284
+ findOrRegisterInflight.delete(key);
285
+ });
286
+ findOrRegisterInflight.set(key, promise);
287
+ return promise;
232
288
  }
233
289
  async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
234
290
  try {
@@ -431,6 +487,7 @@ var lastScopeRefresh = 0;
431
487
  var pluginLogger = null;
432
488
  var pluginConfig;
433
489
  var connectionLogged = false;
490
+ var pinnedAgentName = null;
434
491
  var SCOPE_REFRESH_INTERVAL_MS = 6e4;
435
492
  var cachedMulticornConfig = null;
436
493
  function loadMulticornConfig() {
@@ -453,9 +510,12 @@ function readConfig() {
453
510
  function asString(value) {
454
511
  return typeof value === "string" && value.length > 0 ? value : void 0;
455
512
  }
456
- function resolveAgentName(sessionKey, envOverride) {
457
- if (envOverride !== null && envOverride.trim().length > 0) {
458
- return envOverride.trim();
513
+ function resolveAgentName(sessionKey, configOverride, ctxAgentId) {
514
+ if (configOverride !== null && configOverride.trim().length > 0) {
515
+ return configOverride.trim();
516
+ }
517
+ if (ctxAgentId !== void 0 && ctxAgentId.trim().length > 0) {
518
+ return ctxAgentId.trim();
459
519
  }
460
520
  const parts = sessionKey.split(":");
461
521
  const name = parts[1];
@@ -464,12 +524,23 @@ function resolveAgentName(sessionKey, envOverride) {
464
524
  }
465
525
  return "openclaw";
466
526
  }
527
+ function getAgentName(sessionKey, configOverride, ctxAgentId) {
528
+ if (pinnedAgentName !== null) return pinnedAgentName;
529
+ const resolved = resolveAgentName(sessionKey, configOverride, ctxAgentId);
530
+ if (resolved !== "openclaw") {
531
+ pinnedAgentName = resolved;
532
+ }
533
+ return resolved;
534
+ }
467
535
  async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
468
- if (agentRecord !== null && Date.now() - lastScopeRefresh < SCOPE_REFRESH_INTERVAL_MS) {
536
+ if (agentRecord !== null && agentRecord.name === agentName && Date.now() - lastScopeRefresh < SCOPE_REFRESH_INTERVAL_MS) {
469
537
  return "ready";
470
538
  }
539
+ if (agentRecord !== null && agentRecord.name !== agentName) {
540
+ agentRecord = null;
541
+ }
471
542
  if (agentRecord === null) {
472
- const cached = await loadCachedScopes(agentName);
543
+ const cached = await loadCachedScopes(agentName, apiKey);
473
544
  if (cached !== null && cached.length > 0) {
474
545
  grantedScopes = cached;
475
546
  void findOrRegisterAgent(agentName, apiKey, baseUrl, pluginLogger ?? void 0).then(
@@ -501,7 +572,7 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
501
572
  grantedScopes = scopes;
502
573
  lastScopeRefresh = Date.now();
503
574
  if (scopes.length > 0) {
504
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
575
+ await saveCachedScopes(agentName, agentRecord.id, scopes, apiKey).catch(() => {
505
576
  });
506
577
  }
507
578
  if (!connectionLogged) {
@@ -543,7 +614,7 @@ async function ensureConsent(agentName, apiKey, baseUrl, scope) {
543
614
  pluginLogger ?? void 0
544
615
  );
545
616
  grantedScopes = scopes;
546
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
617
+ await saveCachedScopes(agentName, agentRecord.id, scopes, apiKey).catch(() => {
547
618
  });
548
619
  } finally {
549
620
  consentInProgress = false;
@@ -632,7 +703,7 @@ async function beforeToolCall(event, ctx) {
632
703
  console.error("[SHIELD] DECISION: allow (no API key)");
633
704
  return void 0;
634
705
  }
635
- const agentName = resolveAgentName(ctx.sessionKey ?? "", config.agentName);
706
+ const agentName = getAgentName(ctx.sessionKey ?? "", config.agentName, ctx.agentId);
636
707
  const readiness = await ensureAgent(agentName, config.apiKey, config.baseUrl, config.failMode);
637
708
  console.error("[SHIELD] ensureAgent result: " + JSON.stringify(readiness));
638
709
  if (readiness === "block") {
@@ -694,7 +765,7 @@ async function beforeToolCall(event, ctx) {
694
765
  grantedScopes = scopes;
695
766
  lastScopeRefresh = Date.now();
696
767
  if (Array.isArray(scopes) && scopes.length > 0) {
697
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
768
+ await saveCachedScopes(agentName, agentRecord.id, scopes, config.apiKey).catch(() => {
698
769
  });
699
770
  }
700
771
  }
@@ -702,10 +773,11 @@ async function beforeToolCall(event, ctx) {
702
773
  return void 0;
703
774
  }
704
775
  if (permissionResult.status === "pending" && permissionResult.approvalId !== void 0) {
705
- const dashboardUrl2 = deriveDashboardUrl(config.baseUrl);
776
+ const base2 = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
706
777
  const returnValue2 = {
707
778
  block: true,
708
- blockReason: `Action pending approval. Visit ${dashboardUrl2}approvals to approve or reject, then try again.`
779
+ blockReason: `Action pending approval.
780
+ Visit ${base2}/approvals to approve or reject, then try again.`
709
781
  };
710
782
  console.error("[SHIELD] DECISION: " + JSON.stringify(returnValue2));
711
783
  return returnValue2;
@@ -726,7 +798,7 @@ async function beforeToolCall(event, ctx) {
726
798
  grantedScopes = scopes;
727
799
  lastScopeRefresh = Date.now();
728
800
  if (Array.isArray(scopes) && scopes.length > 0) {
729
- await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
801
+ await saveCachedScopes(agentName, agentRecord.id, scopes, config.apiKey).catch(() => {
730
802
  });
731
803
  }
732
804
  const recheckResult = await checkActionPermission(
@@ -751,16 +823,19 @@ async function beforeToolCall(event, ctx) {
751
823
  grantedScopes = refreshedScopes;
752
824
  lastScopeRefresh = Date.now();
753
825
  if (Array.isArray(refreshedScopes) && refreshedScopes.length > 0) {
754
- await saveCachedScopes(agentName, agentRecord.id, refreshedScopes).catch(() => {
755
- });
826
+ await saveCachedScopes(agentName, agentRecord.id, refreshedScopes, config.apiKey).catch(
827
+ () => {
828
+ }
829
+ );
756
830
  }
757
831
  console.error("[SHIELD] DECISION: allow (re-check after consent)");
758
832
  return void 0;
759
833
  }
760
834
  }
761
835
  const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
762
- const dashboardUrl = deriveDashboardUrl(config.baseUrl);
763
- const reason = `${capitalizedService} ${mapping.permissionLevel} access is not allowed. Check pending approvals at ${dashboardUrl}/approvals `;
836
+ const base = deriveDashboardUrl(config.baseUrl).replace(/\/+$/, "");
837
+ const reason = `${capitalizedService} ${mapping.permissionLevel} access is not allowed. Check pending approvals at:
838
+ ${base}/approvals`;
764
839
  const returnValue = { block: true, blockReason: reason };
765
840
  console.error("[SHIELD] DECISION: " + JSON.stringify(returnValue));
766
841
  return returnValue;
@@ -773,7 +848,7 @@ async function beforeToolCall(event, ctx) {
773
848
  function afterToolCall(event, ctx) {
774
849
  const config = readConfig();
775
850
  if (config.apiKey.length === 0) return Promise.resolve();
776
- const agentName = resolveAgentName(ctx.sessionKey ?? "", config.agentName);
851
+ const agentName = getAgentName(ctx.sessionKey ?? "", config.agentName, ctx.agentId);
777
852
  const mapping = mapToolToScope(event.toolName);
778
853
  void logAction(
779
854
  {
@@ -801,11 +876,14 @@ var plugin = {
801
876
  pluginLogger = api.logger;
802
877
  pluginConfig = api.pluginConfig;
803
878
  cachedMulticornConfig = loadMulticornConfig();
879
+ const config = readConfig();
880
+ if (config.agentName !== null) {
881
+ pinnedAgentName = config.agentName;
882
+ }
804
883
  console.error("[SHIELD-DIAG] cachedMulticornConfig: " + JSON.stringify(cachedMulticornConfig));
805
884
  api.on("before_tool_call", beforeToolCall, { priority: 10 });
806
885
  api.on("after_tool_call", afterToolCall);
807
886
  api.logger.info("Multicorn Shield plugin registered.");
808
- const config = readConfig();
809
887
  if (config.apiKey.length === 0) {
810
888
  api.logger.error(
811
889
  "Multicorn Shield: No API key found. Run 'npx multicorn-proxy init' or set MULTICORN_API_KEY."
@@ -829,6 +907,7 @@ function resetState() {
829
907
  pluginConfig = void 0;
830
908
  cachedMulticornConfig = null;
831
909
  connectionLogged = false;
910
+ pinnedAgentName = null;
832
911
  }
833
912
 
834
913
  export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",