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.
|
package/dist/multicorn-proxy.js
CHANGED
|
@@ -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)
|
|
647
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
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:
|
|
163
|
+
return { shouldBlock: true };
|
|
164
164
|
}
|
|
165
165
|
if (status >= 500 && status < 600) {
|
|
166
|
-
const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action
|
|
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:
|
|
169
|
+
return { shouldBlock: true };
|
|
170
170
|
}
|
|
171
|
-
return { shouldBlock:
|
|
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
|
|
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:
|
|
170
|
+
return { shouldBlock: true };
|
|
171
171
|
}
|
|
172
172
|
if (status >= 500 && status < 600) {
|
|
173
|
-
const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action
|
|
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:
|
|
177
|
+
return { shouldBlock: true };
|
|
178
178
|
}
|
|
179
|
-
return { shouldBlock:
|
|
179
|
+
return { shouldBlock: true };
|
|
180
180
|
}
|
|
181
181
|
async function findAgentByName(agentName, apiKey, baseUrl, logger) {
|
|
182
182
|
try {
|