multicorn-shield 0.1.13 → 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
@@ -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.
@@ -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
  );
@@ -744,6 +802,9 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
744
802
  return { id: "", name: agentName, scopes: cachedScopes };
745
803
  }
746
804
  let agent = await findAgentByName(agentName, apiKey, baseUrl);
805
+ if (agent?.authInvalid) {
806
+ return agent;
807
+ }
747
808
  if (agent === null) {
748
809
  try {
749
810
  logger.info("Agent not found. Registering.", { agent: agentName });
@@ -751,6 +812,9 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
751
812
  agent = { id, name: agentName, scopes: [] };
752
813
  logger.info("Agent registered.", { agent: agentName, id });
753
814
  } catch (error) {
815
+ if (error instanceof ShieldAuthError) {
816
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
817
+ }
754
818
  const detail = error instanceof Error ? error.message : String(error);
755
819
  logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
756
820
  error: detail
@@ -815,6 +879,7 @@ function createProxyServer(config) {
815
879
  let spendingChecker = null;
816
880
  let grantedScopes = [];
817
881
  let agentId = "";
882
+ let authInvalid = false;
818
883
  let refreshTimer = null;
819
884
  let consentInProgress = false;
820
885
  const pendingLines = [];
@@ -867,40 +932,28 @@ function createProxyServer(config) {
867
932
  if (request.method !== "tools/call") return null;
868
933
  const toolParams = extractToolCallParams(request);
869
934
  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);
935
+ try {
936
+ if (authInvalid) {
937
+ const blocked = buildAuthErrorResponse(request.id);
896
938
  return JSON.stringify(blocked);
897
939
  }
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) {
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) {
904
957
  if (actionLogger !== null) {
905
958
  if (!config.agentName || config.agentName.trim().length === 0) {
906
959
  process.stderr.write(
@@ -915,29 +968,60 @@ function createProxyServer(config) {
915
968
  });
916
969
  }
917
970
  }
918
- const blocked = buildSpendingBlockedResponse(
919
- request.id,
920
- spendResult.reason ?? "spending limit exceeded",
921
- config.dashboardUrl
971
+ return JSON.stringify(
972
+ buildBlockedResponse(request.id, service, "execute", config.dashboardUrl)
922
973
  );
923
- return JSON.stringify(blocked);
924
974
  }
925
- spendingChecker.recordSpend(costCents);
926
975
  }
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
- });
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
+ }
938
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);
939
1024
  }
940
- return null;
941
1025
  }
942
1026
  async function processLine(line) {
943
1027
  const childProcess = child;
@@ -989,6 +1073,7 @@ function createProxyServer(config) {
989
1073
  );
990
1074
  agentId = agentRecord.id;
991
1075
  grantedScopes = agentRecord.scopes;
1076
+ authInvalid = agentRecord.authInvalid === true;
992
1077
  config.logger.info("Agent resolved.", {
993
1078
  agent: config.agentName,
994
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 {
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.1.13",
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",