multicorn-shield 0.1.2 → 0.1.5
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/dist/multicorn-proxy.js +38 -20
- package/dist/openclaw-hook/HOOK.md +18 -0
- package/dist/openclaw-hook/handler.js +78 -21
- package/dist/openclaw-plugin/index.js +190 -33
- package/package.json +1 -1
package/dist/multicorn-proxy.js
CHANGED
|
@@ -128,6 +128,11 @@ function validateScopeAccess(grantedScopes, requested) {
|
|
|
128
128
|
reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
function hasScope(grantedScopes, requested) {
|
|
132
|
+
return grantedScopes.some(
|
|
133
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
134
|
+
);
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
// src/logger/action-logger.ts
|
|
133
138
|
function createActionLogger(config) {
|
|
@@ -639,9 +644,9 @@ function openBrowser(url) {
|
|
|
639
644
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
640
645
|
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
641
646
|
}
|
|
642
|
-
async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger) {
|
|
643
|
-
const
|
|
644
|
-
const consentUrl = buildConsentUrl(agentName,
|
|
647
|
+
async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger, scope) {
|
|
648
|
+
const scopeStrings = scope ? [`${scope.service}:${scope.permissionLevel}`] : detectScopeHints();
|
|
649
|
+
const consentUrl = buildConsentUrl(agentName, scopeStrings, dashboardUrl);
|
|
645
650
|
logger.info("Opening consent page in your browser.", { url: consentUrl });
|
|
646
651
|
process.stderr.write(
|
|
647
652
|
`
|
|
@@ -693,11 +698,12 @@ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger) {
|
|
|
693
698
|
return { ...agent, scopes };
|
|
694
699
|
}
|
|
695
700
|
function buildConsentUrl(agentName, scopes, dashboardUrl) {
|
|
701
|
+
const base = dashboardUrl.replace(/\/+$/, "");
|
|
696
702
|
const params = new URLSearchParams({ agent: agentName });
|
|
697
703
|
if (scopes.length > 0) {
|
|
698
704
|
params.set("scopes", scopes.join(","));
|
|
699
705
|
}
|
|
700
|
-
return `${
|
|
706
|
+
return `${base}/consent?${params.toString()}`;
|
|
701
707
|
}
|
|
702
708
|
function detectScopeHints() {
|
|
703
709
|
return [];
|
|
@@ -746,7 +752,9 @@ function createProxyServer(config) {
|
|
|
746
752
|
let consentInProgress = false;
|
|
747
753
|
const pendingLines = [];
|
|
748
754
|
let draining = false;
|
|
755
|
+
let stopped = false;
|
|
749
756
|
async function refreshScopes() {
|
|
757
|
+
if (stopped) return;
|
|
750
758
|
if (agentId.length === 0) return;
|
|
751
759
|
try {
|
|
752
760
|
const scopes = await fetchGrantedScopes(agentId, config.apiKey, config.baseUrl);
|
|
@@ -761,18 +769,24 @@ function createProxyServer(config) {
|
|
|
761
769
|
});
|
|
762
770
|
}
|
|
763
771
|
}
|
|
764
|
-
async function ensureConsent() {
|
|
765
|
-
if (grantedScopes.length > 0 || consentInProgress) return;
|
|
772
|
+
async function ensureConsent(requestedScope) {
|
|
766
773
|
if (agentId.length === 0) return;
|
|
774
|
+
if (requestedScope !== void 0) {
|
|
775
|
+
if (hasScope(grantedScopes, requestedScope) || consentInProgress) return;
|
|
776
|
+
} else {
|
|
777
|
+
if (grantedScopes.length > 0 || consentInProgress) return;
|
|
778
|
+
}
|
|
767
779
|
consentInProgress = true;
|
|
768
780
|
try {
|
|
781
|
+
const scopeParam = requestedScope !== void 0 ? { service: requestedScope.service, permissionLevel: requestedScope.permissionLevel } : void 0;
|
|
769
782
|
const scopes = await waitForConsent(
|
|
770
783
|
agentId,
|
|
771
784
|
config.agentName,
|
|
772
785
|
config.apiKey,
|
|
773
786
|
config.baseUrl,
|
|
774
787
|
config.dashboardUrl,
|
|
775
|
-
config.logger
|
|
788
|
+
config.logger,
|
|
789
|
+
scopeParam
|
|
776
790
|
);
|
|
777
791
|
grantedScopes = scopes;
|
|
778
792
|
await saveCachedScopes(config.agentName, agentId, scopes);
|
|
@@ -786,7 +800,6 @@ function createProxyServer(config) {
|
|
|
786
800
|
if (request.method !== "tools/call") return null;
|
|
787
801
|
const toolParams = extractToolCallParams(request);
|
|
788
802
|
if (toolParams === null) return null;
|
|
789
|
-
await ensureConsent();
|
|
790
803
|
const service = extractServiceFromToolName(toolParams.name);
|
|
791
804
|
const action = extractActionFromToolName(toolParams.name);
|
|
792
805
|
const requestedScope = { service, permissionLevel: "execute" };
|
|
@@ -797,20 +810,24 @@ function createProxyServer(config) {
|
|
|
797
810
|
allowed: validation.allowed
|
|
798
811
|
});
|
|
799
812
|
if (!validation.allowed) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
813
|
+
await ensureConsent(requestedScope);
|
|
814
|
+
const revalidation = validateScopeAccess(grantedScopes, requestedScope);
|
|
815
|
+
if (!revalidation.allowed) {
|
|
816
|
+
if (actionLogger !== null) {
|
|
817
|
+
if (!config.agentName || config.agentName.trim().length === 0) {
|
|
818
|
+
process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
|
|
819
|
+
} else {
|
|
820
|
+
await actionLogger.logAction({
|
|
821
|
+
agent: config.agentName,
|
|
822
|
+
service,
|
|
823
|
+
actionType: action,
|
|
824
|
+
status: "blocked"
|
|
825
|
+
});
|
|
826
|
+
}
|
|
810
827
|
}
|
|
828
|
+
const blocked = buildBlockedResponse(request.id, service, "execute", config.dashboardUrl);
|
|
829
|
+
return JSON.stringify(blocked);
|
|
811
830
|
}
|
|
812
|
-
const blocked = buildBlockedResponse(request.id, service, "execute", config.dashboardUrl);
|
|
813
|
-
return JSON.stringify(blocked);
|
|
814
831
|
}
|
|
815
832
|
if (spendingChecker !== null) {
|
|
816
833
|
const costCents = extractCostCents(toolParams.arguments);
|
|
@@ -880,6 +897,7 @@ function createProxyServer(config) {
|
|
|
880
897
|
void drainQueue();
|
|
881
898
|
}
|
|
882
899
|
async function stop() {
|
|
900
|
+
stopped = true;
|
|
883
901
|
if (refreshTimer !== null) {
|
|
884
902
|
clearInterval(refreshTimer);
|
|
885
903
|
refreshTimer = null;
|
|
@@ -25,6 +25,24 @@ metadata:
|
|
|
25
25
|
> openclaw gateway restart
|
|
26
26
|
> ```
|
|
27
27
|
>
|
|
28
|
+
> **Plugin Configuration:** Configure the plugin in `~/.openclaw/openclaw.json`:
|
|
29
|
+
>
|
|
30
|
+
> ```json
|
|
31
|
+
> {
|
|
32
|
+
> "plugins": {
|
|
33
|
+
> "entries": {
|
|
34
|
+
> "multicorn-shield": {
|
|
35
|
+
> "enabled": true,
|
|
36
|
+
> "env": {
|
|
37
|
+
> "MULTICORN_API_KEY": "mcs_your_key_here",
|
|
38
|
+
> "MULTICORN_BASE_URL": "https://api.multicorn.ai"
|
|
39
|
+
> }
|
|
40
|
+
> }
|
|
41
|
+
> }
|
|
42
|
+
> }
|
|
43
|
+
> }
|
|
44
|
+
> ```
|
|
45
|
+
>
|
|
28
46
|
> See the plugin README for full instructions.
|
|
29
47
|
|
|
30
48
|
Governance layer for OpenClaw agents. Every tool call is checked against your Shield permissions before it runs. Blocked actions never reach the tool. All activity - approved and blocked - shows up in your Shield dashboard.
|
|
@@ -123,6 +123,7 @@ function isScopesCacheFile(value) {
|
|
|
123
123
|
// src/openclaw/shield-client.ts
|
|
124
124
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
125
125
|
var AUTH_HEADER = "X-Multicorn-Key";
|
|
126
|
+
var authErrorLogged = false;
|
|
126
127
|
function isApiSuccess(value) {
|
|
127
128
|
if (typeof value !== "object" || value === null) return false;
|
|
128
129
|
const obj = value;
|
|
@@ -143,13 +144,42 @@ function isPermissionEntry(value) {
|
|
|
143
144
|
const obj = value;
|
|
144
145
|
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");
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
+
function handleHttpError(status, logger, retryDelaySeconds) {
|
|
148
|
+
if (status === 401 || status === 403) {
|
|
149
|
+
if (!authErrorLogged) {
|
|
150
|
+
authErrorLogged = true;
|
|
151
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in your OpenClaw config (~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.env.MULTICORN_API_KEY). Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
152
|
+
process.stderr.write(`${errorMsg}
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
return { shouldBlock: true };
|
|
156
|
+
}
|
|
157
|
+
if (status === 429) {
|
|
158
|
+
{
|
|
159
|
+
const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
|
|
160
|
+
process.stderr.write(`${rateLimitMsg}
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
return { shouldBlock: false };
|
|
164
|
+
}
|
|
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.`;
|
|
167
|
+
process.stderr.write(`${serverErrorMsg}
|
|
168
|
+
`);
|
|
169
|
+
return { shouldBlock: false };
|
|
170
|
+
}
|
|
171
|
+
return { shouldBlock: false };
|
|
172
|
+
}
|
|
173
|
+
async function findAgentByName(agentName, apiKey, baseUrl, logger) {
|
|
147
174
|
try {
|
|
148
175
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
149
176
|
headers: { [AUTH_HEADER]: apiKey },
|
|
150
177
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
151
178
|
});
|
|
152
|
-
if (!response.ok)
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
handleHttpError(response.status, logger);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
153
183
|
const body = await response.json();
|
|
154
184
|
if (!isApiSuccess(body)) return null;
|
|
155
185
|
const agents = body.data;
|
|
@@ -160,7 +190,7 @@ async function findAgentByName(agentName, apiKey, baseUrl) {
|
|
|
160
190
|
return null;
|
|
161
191
|
}
|
|
162
192
|
}
|
|
163
|
-
async function registerAgent(agentName, apiKey, baseUrl) {
|
|
193
|
+
async function registerAgent(agentName, apiKey, baseUrl, logger) {
|
|
164
194
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
165
195
|
method: "POST",
|
|
166
196
|
headers: {
|
|
@@ -171,6 +201,7 @@ async function registerAgent(agentName, apiKey, baseUrl) {
|
|
|
171
201
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
172
202
|
});
|
|
173
203
|
if (!response.ok) {
|
|
204
|
+
handleHttpError(response.status);
|
|
174
205
|
throw new Error(
|
|
175
206
|
`Failed to register agent "${agentName}": service returned ${String(response.status)}.`
|
|
176
207
|
);
|
|
@@ -181,23 +212,26 @@ async function registerAgent(agentName, apiKey, baseUrl) {
|
|
|
181
212
|
}
|
|
182
213
|
return body.data.id;
|
|
183
214
|
}
|
|
184
|
-
async function findOrRegisterAgent(agentName, apiKey, baseUrl) {
|
|
185
|
-
const existing = await findAgentByName(agentName, apiKey, baseUrl);
|
|
215
|
+
async function findOrRegisterAgent(agentName, apiKey, baseUrl, logger) {
|
|
216
|
+
const existing = await findAgentByName(agentName, apiKey, baseUrl, logger);
|
|
186
217
|
if (existing !== null) return existing;
|
|
187
218
|
try {
|
|
188
|
-
const id = await registerAgent(agentName, apiKey, baseUrl);
|
|
219
|
+
const id = await registerAgent(agentName, apiKey, baseUrl, logger);
|
|
189
220
|
return { id, name: agentName };
|
|
190
221
|
} catch {
|
|
191
222
|
return null;
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
|
-
async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
225
|
+
async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
|
|
195
226
|
try {
|
|
196
227
|
const response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
|
|
197
228
|
headers: { [AUTH_HEADER]: apiKey },
|
|
198
229
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
199
230
|
});
|
|
200
|
-
if (!response.ok)
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
handleHttpError(response.status, logger);
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
201
235
|
const body = await response.json();
|
|
202
236
|
if (!isApiSuccess(body)) return [];
|
|
203
237
|
const detail = body.data;
|
|
@@ -215,7 +249,7 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
|
215
249
|
return [];
|
|
216
250
|
}
|
|
217
251
|
}
|
|
218
|
-
async function logAction(payload, apiKey, baseUrl) {
|
|
252
|
+
async function logAction(payload, apiKey, baseUrl, logger) {
|
|
219
253
|
try {
|
|
220
254
|
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
221
255
|
method: "POST",
|
|
@@ -227,10 +261,7 @@ async function logAction(payload, apiKey, baseUrl) {
|
|
|
227
261
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
228
262
|
});
|
|
229
263
|
if (!response.ok) {
|
|
230
|
-
|
|
231
|
-
`[multicorn-shield] Action log failed: HTTP ${String(response.status)}.
|
|
232
|
-
`
|
|
233
|
-
);
|
|
264
|
+
handleHttpError(response.status, logger);
|
|
234
265
|
}
|
|
235
266
|
} catch (error) {
|
|
236
267
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -261,9 +292,12 @@ function deriveDashboardUrl(baseUrl) {
|
|
|
261
292
|
return "https://app.multicorn.ai";
|
|
262
293
|
}
|
|
263
294
|
}
|
|
264
|
-
function buildConsentUrl(agentName, dashboardUrl) {
|
|
295
|
+
function buildConsentUrl(agentName, dashboardUrl, scope) {
|
|
265
296
|
const base = dashboardUrl.replace(/\/+$/, "");
|
|
266
297
|
const params = new URLSearchParams({ agent: agentName });
|
|
298
|
+
if (scope) {
|
|
299
|
+
params.set("scopes", `${scope.service}:${scope.permissionLevel}`);
|
|
300
|
+
}
|
|
267
301
|
return `${base}/consent?${params.toString()}`;
|
|
268
302
|
}
|
|
269
303
|
function openBrowser(url) {
|
|
@@ -279,9 +313,9 @@ ${url}
|
|
|
279
313
|
);
|
|
280
314
|
}
|
|
281
315
|
}
|
|
282
|
-
async function waitForConsent(agentId, agentName, apiKey, baseUrl) {
|
|
316
|
+
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
283
317
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
284
|
-
const consentUrl = buildConsentUrl(agentName, dashboardUrl);
|
|
318
|
+
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
285
319
|
process.stderr.write(
|
|
286
320
|
`[multicorn-shield] Opening consent page...
|
|
287
321
|
${consentUrl}
|
|
@@ -292,7 +326,7 @@ Waiting for you to grant access in the Multicorn dashboard...
|
|
|
292
326
|
const deadline = Date.now() + POLL_TIMEOUT_MS2;
|
|
293
327
|
while (Date.now() < deadline) {
|
|
294
328
|
await sleep(POLL_INTERVAL_MS2);
|
|
295
|
-
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
|
|
329
|
+
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl, logger);
|
|
296
330
|
if (scopes.length > 0) {
|
|
297
331
|
process.stderr.write("[multicorn-shield] Permissions granted.\n");
|
|
298
332
|
return scopes;
|
|
@@ -306,6 +340,13 @@ function sleep(ms) {
|
|
|
306
340
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
307
341
|
}
|
|
308
342
|
|
|
343
|
+
// src/scopes/scope-validator.ts
|
|
344
|
+
function hasScope(grantedScopes2, requested) {
|
|
345
|
+
return grantedScopes2.some(
|
|
346
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
309
350
|
// src/openclaw/hook/handler.ts
|
|
310
351
|
var agentRecord = null;
|
|
311
352
|
var grantedScopes = [];
|
|
@@ -368,11 +409,20 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
|
|
|
368
409
|
}
|
|
369
410
|
return "ready";
|
|
370
411
|
}
|
|
371
|
-
async function ensureConsent(agentName, apiKey, baseUrl) {
|
|
372
|
-
if (
|
|
412
|
+
async function ensureConsent(agentName, apiKey, baseUrl, scope) {
|
|
413
|
+
if (agentRecord === null) return;
|
|
414
|
+
if (scope !== void 0) {
|
|
415
|
+
const requestedScope = {
|
|
416
|
+
service: scope.service,
|
|
417
|
+
permissionLevel: scope.permissionLevel
|
|
418
|
+
};
|
|
419
|
+
if (hasScope(grantedScopes, requestedScope) || consentInProgress) return;
|
|
420
|
+
} else {
|
|
421
|
+
if (grantedScopes.length > 0 || consentInProgress) return;
|
|
422
|
+
}
|
|
373
423
|
consentInProgress = true;
|
|
374
424
|
try {
|
|
375
|
-
const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl);
|
|
425
|
+
const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl, scope);
|
|
376
426
|
grantedScopes = scopes;
|
|
377
427
|
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
378
428
|
});
|
|
@@ -382,6 +432,7 @@ async function ensureConsent(agentName, apiKey, baseUrl) {
|
|
|
382
432
|
}
|
|
383
433
|
function isPermitted(event) {
|
|
384
434
|
const mapping = mapToolToScope(event.context.toolName);
|
|
435
|
+
if (!grantedScopes || grantedScopes.length === 0) return false;
|
|
385
436
|
return grantedScopes.some(
|
|
386
437
|
(scope) => scope.service === mapping.service && scope.permissionLevel === mapping.permissionLevel
|
|
387
438
|
);
|
|
@@ -406,8 +457,14 @@ var handler = async (event) => {
|
|
|
406
457
|
if (readiness === "skip") {
|
|
407
458
|
return;
|
|
408
459
|
}
|
|
409
|
-
await ensureConsent(agentName, config.apiKey, config.baseUrl);
|
|
410
460
|
const mapping = mapToolToScope(event.context.toolName);
|
|
461
|
+
const requestedScope = {
|
|
462
|
+
service: mapping.service,
|
|
463
|
+
permissionLevel: mapping.permissionLevel
|
|
464
|
+
};
|
|
465
|
+
if (!hasScope(grantedScopes, requestedScope)) {
|
|
466
|
+
await ensureConsent(agentName, config.apiKey, config.baseUrl, mapping);
|
|
467
|
+
}
|
|
411
468
|
const permitted = isPermitted(event);
|
|
412
469
|
if (!permitted) {
|
|
413
470
|
const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import { join } from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
3
5
|
import { homedir } from 'os';
|
|
6
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
7
|
import { spawn } from 'child_process';
|
|
5
8
|
|
|
6
9
|
// Multicorn Shield plugin for OpenClaw - https://multicorn.ai
|
|
7
10
|
|
|
11
|
+
|
|
8
12
|
// src/openclaw/tool-mapper.ts
|
|
9
13
|
var TOOL_MAP = {
|
|
10
14
|
// OpenClaw built-in tools
|
|
@@ -127,6 +131,7 @@ var AUTH_HEADER = "X-Multicorn-Key";
|
|
|
127
131
|
var POLL_INTERVAL_MS = 3e3;
|
|
128
132
|
var MAX_POLLS = 100;
|
|
129
133
|
var POLL_TIMEOUT_MS = POLL_INTERVAL_MS * MAX_POLLS;
|
|
134
|
+
var authErrorLogged = false;
|
|
130
135
|
function isApiSuccess(value) {
|
|
131
136
|
if (typeof value !== "object" || value === null) return false;
|
|
132
137
|
const obj = value;
|
|
@@ -152,13 +157,50 @@ function isApprovalResponse(value) {
|
|
|
152
157
|
const obj = value;
|
|
153
158
|
return typeof obj["id"] === "string" && typeof obj["status"] === "string" && ["pending", "approved", "rejected", "expired"].includes(obj["status"]) && (obj["decided_at"] === null || typeof obj["decided_at"] === "string");
|
|
154
159
|
}
|
|
155
|
-
|
|
160
|
+
function handleHttpError(status, logger, retryDelaySeconds) {
|
|
161
|
+
if (status === 401 || status === 403) {
|
|
162
|
+
if (!authErrorLogged) {
|
|
163
|
+
authErrorLogged = true;
|
|
164
|
+
const errorMsg = "[multicorn-shield] ERROR: Authentication failed. Your MULTICORN_API_KEY is invalid or expired. Check the key in your OpenClaw config (~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.env.MULTICORN_API_KEY). Get a valid key from your Multicorn dashboard (Settings \u2192 API Keys).";
|
|
165
|
+
logger?.error(errorMsg);
|
|
166
|
+
process.stderr.write(`${errorMsg}
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
return { shouldBlock: true };
|
|
170
|
+
}
|
|
171
|
+
if (status === 429) {
|
|
172
|
+
if (retryDelaySeconds !== void 0) {
|
|
173
|
+
const rateLimitMsg = `[multicorn-shield] Rate limited by Shield API. Retrying in ${String(retryDelaySeconds)}s.`;
|
|
174
|
+
logger?.warn(rateLimitMsg);
|
|
175
|
+
process.stderr.write(`${rateLimitMsg}
|
|
176
|
+
`);
|
|
177
|
+
} else {
|
|
178
|
+
const rateLimitMsg = "[multicorn-shield] Rate limited by Shield API. Action was not checked \u2014 proceeding with fail-open.";
|
|
179
|
+
logger?.warn(rateLimitMsg);
|
|
180
|
+
process.stderr.write(`${rateLimitMsg}
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
return { shouldBlock: false };
|
|
184
|
+
}
|
|
185
|
+
if (status >= 500 && status < 600) {
|
|
186
|
+
const serverErrorMsg = `[multicorn-shield] Shield API error (${String(status)}). Action was not checked \u2014 proceeding with fail-open.`;
|
|
187
|
+
logger?.warn(serverErrorMsg);
|
|
188
|
+
process.stderr.write(`${serverErrorMsg}
|
|
189
|
+
`);
|
|
190
|
+
return { shouldBlock: false };
|
|
191
|
+
}
|
|
192
|
+
return { shouldBlock: false };
|
|
193
|
+
}
|
|
194
|
+
async function findAgentByName(agentName, apiKey, baseUrl, logger) {
|
|
156
195
|
try {
|
|
157
196
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
158
197
|
headers: { [AUTH_HEADER]: apiKey },
|
|
159
198
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
160
199
|
});
|
|
161
|
-
if (!response.ok)
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
handleHttpError(response.status, logger);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
162
204
|
const body = await response.json();
|
|
163
205
|
if (!isApiSuccess(body)) return null;
|
|
164
206
|
const agents = body.data;
|
|
@@ -169,7 +211,7 @@ async function findAgentByName(agentName, apiKey, baseUrl) {
|
|
|
169
211
|
return null;
|
|
170
212
|
}
|
|
171
213
|
}
|
|
172
|
-
async function registerAgent(agentName, apiKey, baseUrl) {
|
|
214
|
+
async function registerAgent(agentName, apiKey, baseUrl, logger) {
|
|
173
215
|
const response = await fetch(`${baseUrl}/api/v1/agents`, {
|
|
174
216
|
method: "POST",
|
|
175
217
|
headers: {
|
|
@@ -180,6 +222,7 @@ async function registerAgent(agentName, apiKey, baseUrl) {
|
|
|
180
222
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
181
223
|
});
|
|
182
224
|
if (!response.ok) {
|
|
225
|
+
handleHttpError(response.status, logger);
|
|
183
226
|
throw new Error(
|
|
184
227
|
`Failed to register agent "${agentName}": service returned ${String(response.status)}.`
|
|
185
228
|
);
|
|
@@ -190,23 +233,26 @@ async function registerAgent(agentName, apiKey, baseUrl) {
|
|
|
190
233
|
}
|
|
191
234
|
return body.data.id;
|
|
192
235
|
}
|
|
193
|
-
async function findOrRegisterAgent(agentName, apiKey, baseUrl) {
|
|
194
|
-
const existing = await findAgentByName(agentName, apiKey, baseUrl);
|
|
236
|
+
async function findOrRegisterAgent(agentName, apiKey, baseUrl, logger) {
|
|
237
|
+
const existing = await findAgentByName(agentName, apiKey, baseUrl, logger);
|
|
195
238
|
if (existing !== null) return existing;
|
|
196
239
|
try {
|
|
197
|
-
const id = await registerAgent(agentName, apiKey, baseUrl);
|
|
240
|
+
const id = await registerAgent(agentName, apiKey, baseUrl, logger);
|
|
198
241
|
return { id, name: agentName };
|
|
199
242
|
} catch {
|
|
200
243
|
return null;
|
|
201
244
|
}
|
|
202
245
|
}
|
|
203
|
-
async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
246
|
+
async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
|
|
204
247
|
try {
|
|
205
248
|
const response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
|
|
206
249
|
headers: { [AUTH_HEADER]: apiKey },
|
|
207
250
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
208
251
|
});
|
|
209
|
-
if (!response.ok)
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
handleHttpError(response.status, logger);
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
210
256
|
const body = await response.json();
|
|
211
257
|
if (!isApiSuccess(body)) return [];
|
|
212
258
|
const detail = body.data;
|
|
@@ -224,7 +270,7 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
|
|
|
224
270
|
return [];
|
|
225
271
|
}
|
|
226
272
|
}
|
|
227
|
-
async function checkActionPermission(payload, apiKey, baseUrl) {
|
|
273
|
+
async function checkActionPermission(payload, apiKey, baseUrl, logger) {
|
|
228
274
|
try {
|
|
229
275
|
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
230
276
|
method: "POST",
|
|
@@ -250,6 +296,14 @@ async function checkActionPermission(payload, apiKey, baseUrl) {
|
|
|
250
296
|
}
|
|
251
297
|
return { status: "pending", approvalId };
|
|
252
298
|
}
|
|
299
|
+
if (response.status === 401 || response.status === 403) {
|
|
300
|
+
handleHttpError(response.status, logger);
|
|
301
|
+
return { status: "blocked" };
|
|
302
|
+
}
|
|
303
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
304
|
+
handleHttpError(response.status, logger);
|
|
305
|
+
return { status: "blocked" };
|
|
306
|
+
}
|
|
253
307
|
return { status: "blocked" };
|
|
254
308
|
} catch {
|
|
255
309
|
return { status: "blocked" };
|
|
@@ -270,6 +324,14 @@ async function pollApprovalStatus(approvalId, apiKey, baseUrl, logger) {
|
|
|
270
324
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
271
325
|
});
|
|
272
326
|
if (!response.ok) {
|
|
327
|
+
if (response.status === 401 || response.status === 403) {
|
|
328
|
+
handleHttpError(response.status, logger);
|
|
329
|
+
return "timeout";
|
|
330
|
+
}
|
|
331
|
+
if (response.status === 429 || response.status >= 500 && response.status < 600) {
|
|
332
|
+
const retryDelay = retry < 2 ? Math.pow(2, retry) : void 0;
|
|
333
|
+
handleHttpError(response.status, logger, retryDelay);
|
|
334
|
+
}
|
|
273
335
|
logDebug?.(
|
|
274
336
|
`Poll ${String(pollCount + 1)} failed: HTTP ${String(response.status)}. Retrying...`
|
|
275
337
|
);
|
|
@@ -326,7 +388,7 @@ async function pollApprovalStatus(approvalId, apiKey, baseUrl, logger) {
|
|
|
326
388
|
}
|
|
327
389
|
return "timeout";
|
|
328
390
|
}
|
|
329
|
-
async function logAction(payload, apiKey, baseUrl) {
|
|
391
|
+
async function logAction(payload, apiKey, baseUrl, logger) {
|
|
330
392
|
try {
|
|
331
393
|
const response = await fetch(`${baseUrl}/api/v1/actions`, {
|
|
332
394
|
method: "POST",
|
|
@@ -338,10 +400,7 @@ async function logAction(payload, apiKey, baseUrl) {
|
|
|
338
400
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
339
401
|
});
|
|
340
402
|
if (!response.ok) {
|
|
341
|
-
|
|
342
|
-
`[multicorn-shield] Action log failed: HTTP ${String(response.status)}.
|
|
343
|
-
`
|
|
344
|
-
);
|
|
403
|
+
handleHttpError(response.status, logger);
|
|
345
404
|
}
|
|
346
405
|
} catch (error) {
|
|
347
406
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -372,9 +431,12 @@ function deriveDashboardUrl(baseUrl) {
|
|
|
372
431
|
return "https://app.multicorn.ai";
|
|
373
432
|
}
|
|
374
433
|
}
|
|
375
|
-
function buildConsentUrl(agentName, dashboardUrl) {
|
|
434
|
+
function buildConsentUrl(agentName, dashboardUrl, scope) {
|
|
376
435
|
const base = dashboardUrl.replace(/\/+$/, "");
|
|
377
436
|
const params = new URLSearchParams({ agent: agentName });
|
|
437
|
+
if (scope) {
|
|
438
|
+
params.set("scopes", `${scope.service}:${scope.permissionLevel}`);
|
|
439
|
+
}
|
|
378
440
|
return `${base}/consent?${params.toString()}`;
|
|
379
441
|
}
|
|
380
442
|
function openBrowser(url) {
|
|
@@ -390,9 +452,9 @@ ${url}
|
|
|
390
452
|
);
|
|
391
453
|
}
|
|
392
454
|
}
|
|
393
|
-
async function waitForConsent(agentId, agentName, apiKey, baseUrl) {
|
|
455
|
+
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
394
456
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
395
|
-
const consentUrl = buildConsentUrl(agentName, dashboardUrl);
|
|
457
|
+
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
396
458
|
process.stderr.write(
|
|
397
459
|
`[multicorn-shield] Opening consent page...
|
|
398
460
|
${consentUrl}
|
|
@@ -403,7 +465,7 @@ Waiting for you to grant access in the Multicorn dashboard...
|
|
|
403
465
|
const deadline = Date.now() + POLL_TIMEOUT_MS2;
|
|
404
466
|
while (Date.now() < deadline) {
|
|
405
467
|
await sleep(POLL_INTERVAL_MS2);
|
|
406
|
-
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
|
|
468
|
+
const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl, logger);
|
|
407
469
|
if (scopes.length > 0) {
|
|
408
470
|
process.stderr.write("[multicorn-shield] Permissions granted.\n");
|
|
409
471
|
return scopes;
|
|
@@ -417,6 +479,13 @@ function sleep(ms) {
|
|
|
417
479
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
418
480
|
}
|
|
419
481
|
|
|
482
|
+
// src/scopes/scope-validator.ts
|
|
483
|
+
function hasScope(grantedScopes2, requested) {
|
|
484
|
+
return grantedScopes2.some(
|
|
485
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
420
489
|
// src/openclaw/plugin/index.ts
|
|
421
490
|
var agentRecord = null;
|
|
422
491
|
var grantedScopes = [];
|
|
@@ -424,15 +493,45 @@ var consentInProgress = false;
|
|
|
424
493
|
var lastScopeRefresh = 0;
|
|
425
494
|
var pluginLogger = null;
|
|
426
495
|
var pluginConfig;
|
|
496
|
+
var connectionLogged = false;
|
|
427
497
|
var SCOPE_REFRESH_INTERVAL_MS = 6e4;
|
|
428
498
|
function readConfig() {
|
|
429
499
|
const pc = pluginConfig ?? {};
|
|
430
|
-
|
|
431
|
-
|
|
500
|
+
let resolvedApiKey = asString(pc["apiKey"]) ?? process.env["MULTICORN_API_KEY"] ?? "";
|
|
501
|
+
let resolvedBaseUrl = asString(pc["baseUrl"]) ?? process.env["MULTICORN_BASE_URL"] ?? "";
|
|
502
|
+
if (!resolvedApiKey) {
|
|
503
|
+
try {
|
|
504
|
+
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
505
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
506
|
+
const config = JSON.parse(configContent);
|
|
507
|
+
const hooks = config["hooks"];
|
|
508
|
+
const internal = hooks?.["internal"];
|
|
509
|
+
const entries = internal?.["entries"];
|
|
510
|
+
const shieldEntry = entries?.["multicorn-shield"];
|
|
511
|
+
const env = shieldEntry?.env;
|
|
512
|
+
if (env) {
|
|
513
|
+
const hookApiKey = asString(env["MULTICORN_API_KEY"]);
|
|
514
|
+
const hookBaseUrl = asString(env["MULTICORN_BASE_URL"]);
|
|
515
|
+
if (hookApiKey) {
|
|
516
|
+
resolvedApiKey = hookApiKey;
|
|
517
|
+
resolvedBaseUrl = resolvedBaseUrl || (hookBaseUrl ?? "https://api.multicorn.ai");
|
|
518
|
+
pluginLogger?.warn(
|
|
519
|
+
"Multicorn Shield: Reading config from hooks.internal.entries. For cleaner setup, set MULTICORN_API_KEY as a system environment variable."
|
|
520
|
+
);
|
|
521
|
+
} else if (hookBaseUrl) {
|
|
522
|
+
resolvedBaseUrl = resolvedBaseUrl || hookBaseUrl;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!resolvedBaseUrl) {
|
|
529
|
+
resolvedBaseUrl = "https://api.multicorn.ai";
|
|
530
|
+
}
|
|
432
531
|
const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? null;
|
|
433
532
|
const failModeRaw = asString(pc["failMode"]) ?? process.env["MULTICORN_FAIL_MODE"] ?? "open";
|
|
434
533
|
const failMode = failModeRaw === "closed" ? "closed" : "open";
|
|
435
|
-
return { apiKey, baseUrl, agentName, failMode };
|
|
534
|
+
return { apiKey: resolvedApiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
|
|
436
535
|
}
|
|
437
536
|
function asString(value) {
|
|
438
537
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -456,15 +555,17 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
|
|
|
456
555
|
const cached = await loadCachedScopes(agentName);
|
|
457
556
|
if (cached !== null && cached.length > 0) {
|
|
458
557
|
grantedScopes = cached;
|
|
459
|
-
void findOrRegisterAgent(agentName, apiKey, baseUrl).then(
|
|
460
|
-
|
|
461
|
-
|
|
558
|
+
void findOrRegisterAgent(agentName, apiKey, baseUrl, pluginLogger ?? void 0).then(
|
|
559
|
+
(record) => {
|
|
560
|
+
if (record !== null) agentRecord = record;
|
|
561
|
+
}
|
|
562
|
+
);
|
|
462
563
|
lastScopeRefresh = Date.now();
|
|
463
564
|
return "ready";
|
|
464
565
|
}
|
|
465
566
|
}
|
|
466
567
|
if (agentRecord === null) {
|
|
467
|
-
const record = await findOrRegisterAgent(agentName, apiKey, baseUrl);
|
|
568
|
+
const record = await findOrRegisterAgent(agentName, apiKey, baseUrl, pluginLogger ?? void 0);
|
|
468
569
|
if (record === null) {
|
|
469
570
|
if (failMode === "closed") {
|
|
470
571
|
return "block";
|
|
@@ -474,20 +575,45 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
|
|
|
474
575
|
}
|
|
475
576
|
agentRecord = record;
|
|
476
577
|
}
|
|
477
|
-
const scopes = await fetchGrantedScopes(
|
|
578
|
+
const scopes = await fetchGrantedScopes(
|
|
579
|
+
agentRecord.id,
|
|
580
|
+
apiKey,
|
|
581
|
+
baseUrl,
|
|
582
|
+
pluginLogger ?? void 0
|
|
583
|
+
);
|
|
478
584
|
grantedScopes = scopes;
|
|
479
585
|
lastScopeRefresh = Date.now();
|
|
480
586
|
if (scopes.length > 0) {
|
|
481
587
|
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
482
588
|
});
|
|
483
589
|
}
|
|
590
|
+
if (!connectionLogged) {
|
|
591
|
+
connectionLogged = true;
|
|
592
|
+
pluginLogger?.info(`Multicorn Shield connected. Agent: ${agentName}`);
|
|
593
|
+
}
|
|
484
594
|
return "ready";
|
|
485
595
|
}
|
|
486
|
-
async function ensureConsent(agentName, apiKey, baseUrl) {
|
|
487
|
-
if (
|
|
596
|
+
async function ensureConsent(agentName, apiKey, baseUrl, scope) {
|
|
597
|
+
if (agentRecord === null) return;
|
|
598
|
+
if (scope !== void 0) {
|
|
599
|
+
const requestedScope = {
|
|
600
|
+
service: scope.service,
|
|
601
|
+
permissionLevel: scope.permissionLevel
|
|
602
|
+
};
|
|
603
|
+
if (hasScope(grantedScopes, requestedScope) || consentInProgress) return;
|
|
604
|
+
} else {
|
|
605
|
+
if (grantedScopes.length > 0 || consentInProgress) return;
|
|
606
|
+
}
|
|
488
607
|
consentInProgress = true;
|
|
489
608
|
try {
|
|
490
|
-
const scopes = await waitForConsent(
|
|
609
|
+
const scopes = await waitForConsent(
|
|
610
|
+
agentRecord.id,
|
|
611
|
+
agentName,
|
|
612
|
+
apiKey,
|
|
613
|
+
baseUrl,
|
|
614
|
+
scope,
|
|
615
|
+
pluginLogger ?? void 0
|
|
616
|
+
);
|
|
491
617
|
grantedScopes = scopes;
|
|
492
618
|
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
493
619
|
});
|
|
@@ -581,7 +707,6 @@ async function beforeToolCall(event, ctx) {
|
|
|
581
707
|
if (readiness === "skip") {
|
|
582
708
|
return void 0;
|
|
583
709
|
}
|
|
584
|
-
await ensureConsent(agentName, config.apiKey, config.baseUrl);
|
|
585
710
|
const command = event.toolName === "exec" && typeof event.params["command"] === "string" ? event.params["command"] : void 0;
|
|
586
711
|
const mapping = mapToolToScope(event.toolName, command);
|
|
587
712
|
pluginLogger?.info(
|
|
@@ -607,9 +732,24 @@ async function beforeToolCall(event, ctx) {
|
|
|
607
732
|
}
|
|
608
733
|
},
|
|
609
734
|
config.apiKey,
|
|
610
|
-
config.baseUrl
|
|
735
|
+
config.baseUrl,
|
|
736
|
+
pluginLogger ?? void 0
|
|
611
737
|
);
|
|
612
738
|
if (permissionResult.status === "approved") {
|
|
739
|
+
if (agentRecord !== null) {
|
|
740
|
+
const scopes = await fetchGrantedScopes(
|
|
741
|
+
agentRecord.id,
|
|
742
|
+
config.apiKey,
|
|
743
|
+
config.baseUrl,
|
|
744
|
+
pluginLogger ?? void 0
|
|
745
|
+
);
|
|
746
|
+
grantedScopes = scopes;
|
|
747
|
+
lastScopeRefresh = Date.now();
|
|
748
|
+
if (scopes.length > 0) {
|
|
749
|
+
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
613
753
|
return void 0;
|
|
614
754
|
}
|
|
615
755
|
if (permissionResult.status === "pending" && permissionResult.approvalId !== void 0) {
|
|
@@ -642,6 +782,13 @@ async function beforeToolCall(event, ctx) {
|
|
|
642
782
|
blockReason: "Approval request timed out after 5 minutes."
|
|
643
783
|
};
|
|
644
784
|
}
|
|
785
|
+
const requestedScope = {
|
|
786
|
+
service: mapping.service,
|
|
787
|
+
permissionLevel: mapping.permissionLevel
|
|
788
|
+
};
|
|
789
|
+
if (!hasScope(grantedScopes, requestedScope) && agentRecord !== null) {
|
|
790
|
+
await ensureConsent(agentName, config.apiKey, config.baseUrl, mapping);
|
|
791
|
+
}
|
|
645
792
|
const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
|
|
646
793
|
const reason = `${capitalizedService} ${mapping.permissionLevel} access is not allowed. Visit the Multicorn Shield dashboard to manage permissions.`;
|
|
647
794
|
return { block: true, blockReason: reason };
|
|
@@ -663,7 +810,8 @@ function afterToolCall(event, ctx) {
|
|
|
663
810
|
}
|
|
664
811
|
},
|
|
665
812
|
config.apiKey,
|
|
666
|
-
config.baseUrl
|
|
813
|
+
config.baseUrl,
|
|
814
|
+
pluginLogger ?? void 0
|
|
667
815
|
);
|
|
668
816
|
return Promise.resolve();
|
|
669
817
|
}
|
|
@@ -678,6 +826,14 @@ var plugin = {
|
|
|
678
826
|
api.on("before_tool_call", beforeToolCall, { priority: 10 });
|
|
679
827
|
api.on("after_tool_call", afterToolCall);
|
|
680
828
|
api.logger.info("Multicorn Shield plugin registered.");
|
|
829
|
+
const config = readConfig();
|
|
830
|
+
if (config.apiKey.length === 0) {
|
|
831
|
+
api.logger.error(
|
|
832
|
+
"Multicorn Shield: No API key found. Set MULTICORN_API_KEY in your OpenClaw config (~/.openclaw/openclaw.json \u2192 plugins.entries.multicorn-shield.env.MULTICORN_API_KEY). Get a key from your Multicorn dashboard (Settings \u2192 API Keys)."
|
|
833
|
+
);
|
|
834
|
+
} else {
|
|
835
|
+
api.logger.info(`Multicorn Shield connecting to ${config.baseUrl}`);
|
|
836
|
+
}
|
|
681
837
|
}
|
|
682
838
|
};
|
|
683
839
|
function register(api) {
|
|
@@ -692,6 +848,7 @@ function resetState() {
|
|
|
692
848
|
lastScopeRefresh = 0;
|
|
693
849
|
pluginLogger = null;
|
|
694
850
|
pluginConfig = void 0;
|
|
851
|
+
connectionLogged = false;
|
|
695
852
|
}
|
|
696
853
|
|
|
697
854
|
export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
|