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
|
[](LICENSE)
|
|
11
11
|
[](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:
|
|
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.
|
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
|
);
|
|
@@ -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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
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 {
|
|
@@ -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
|
|
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 {
|
|
@@ -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);
|