multicorn-shield 0.1.3 → 0.1.6
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
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;
|
|
@@ -295,6 +295,9 @@ function deriveDashboardUrl(baseUrl) {
|
|
|
295
295
|
function buildConsentUrl(agentName, dashboardUrl, scope) {
|
|
296
296
|
const base = dashboardUrl.replace(/\/+$/, "");
|
|
297
297
|
const params = new URLSearchParams({ agent: agentName });
|
|
298
|
+
if (scope) {
|
|
299
|
+
params.set("scopes", `${scope.service}:${scope.permissionLevel}`);
|
|
300
|
+
}
|
|
298
301
|
return `${base}/consent?${params.toString()}`;
|
|
299
302
|
}
|
|
300
303
|
function openBrowser(url) {
|
|
@@ -312,7 +315,7 @@ ${url}
|
|
|
312
315
|
}
|
|
313
316
|
async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
|
|
314
317
|
const dashboardUrl = deriveDashboardUrl(baseUrl);
|
|
315
|
-
const consentUrl = buildConsentUrl(agentName, dashboardUrl);
|
|
318
|
+
const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
|
|
316
319
|
process.stderr.write(
|
|
317
320
|
`[multicorn-shield] Opening consent page...
|
|
318
321
|
${consentUrl}
|
|
@@ -337,6 +340,13 @@ function sleep(ms) {
|
|
|
337
340
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
338
341
|
}
|
|
339
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
|
+
|
|
340
350
|
// src/openclaw/hook/handler.ts
|
|
341
351
|
var agentRecord = null;
|
|
342
352
|
var grantedScopes = [];
|
|
@@ -399,11 +409,20 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
|
|
|
399
409
|
}
|
|
400
410
|
return "ready";
|
|
401
411
|
}
|
|
402
|
-
async function ensureConsent(agentName, apiKey, baseUrl) {
|
|
403
|
-
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
|
+
}
|
|
404
423
|
consentInProgress = true;
|
|
405
424
|
try {
|
|
406
|
-
const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl);
|
|
425
|
+
const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl, scope);
|
|
407
426
|
grantedScopes = scopes;
|
|
408
427
|
await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
|
|
409
428
|
});
|
|
@@ -413,6 +432,7 @@ async function ensureConsent(agentName, apiKey, baseUrl) {
|
|
|
413
432
|
}
|
|
414
433
|
function isPermitted(event) {
|
|
415
434
|
const mapping = mapToolToScope(event.context.toolName);
|
|
435
|
+
if (!grantedScopes || grantedScopes.length === 0) return false;
|
|
416
436
|
return grantedScopes.some(
|
|
417
437
|
(scope) => scope.service === mapping.service && scope.permissionLevel === mapping.permissionLevel
|
|
418
438
|
);
|
|
@@ -437,8 +457,14 @@ var handler = async (event) => {
|
|
|
437
457
|
if (readiness === "skip") {
|
|
438
458
|
return;
|
|
439
459
|
}
|
|
440
|
-
await ensureConsent(agentName, config.apiKey, config.baseUrl);
|
|
441
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
|
+
}
|
|
442
468
|
const permitted = isPermitted(event);
|
|
443
469
|
if (!permitted) {
|
|
444
470
|
const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
|
|
@@ -479,6 +479,13 @@ function sleep(ms) {
|
|
|
479
479
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
480
480
|
}
|
|
481
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
|
+
|
|
482
489
|
// src/openclaw/plugin/index.ts
|
|
483
490
|
var agentRecord = null;
|
|
484
491
|
var grantedScopes = [];
|
|
@@ -492,6 +499,20 @@ function readConfig() {
|
|
|
492
499
|
const pc = pluginConfig ?? {};
|
|
493
500
|
let resolvedApiKey = asString(pc["apiKey"]) ?? process.env["MULTICORN_API_KEY"] ?? "";
|
|
494
501
|
let resolvedBaseUrl = asString(pc["baseUrl"]) ?? process.env["MULTICORN_BASE_URL"] ?? "";
|
|
502
|
+
if (!resolvedApiKey) {
|
|
503
|
+
try {
|
|
504
|
+
const multicornConfigPath = path.join(os.homedir(), ".multicorn", "config.json");
|
|
505
|
+
const multicornConfigContent = fs.readFileSync(multicornConfigPath, "utf-8");
|
|
506
|
+
const multicornConfig = JSON.parse(multicornConfigContent);
|
|
507
|
+
if (multicornConfig && typeof multicornConfig.apiKey === "string" && multicornConfig.apiKey.length > 0) {
|
|
508
|
+
resolvedApiKey = multicornConfig.apiKey;
|
|
509
|
+
if (!resolvedBaseUrl) {
|
|
510
|
+
resolvedBaseUrl = multicornConfig.baseUrl ?? "https://api.multicorn.ai";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
495
516
|
if (!resolvedApiKey) {
|
|
496
517
|
try {
|
|
497
518
|
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
@@ -587,7 +608,16 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
|
|
|
587
608
|
return "ready";
|
|
588
609
|
}
|
|
589
610
|
async function ensureConsent(agentName, apiKey, baseUrl, scope) {
|
|
590
|
-
if (
|
|
611
|
+
if (agentRecord === null) return;
|
|
612
|
+
if (scope !== void 0) {
|
|
613
|
+
const requestedScope = {
|
|
614
|
+
service: scope.service,
|
|
615
|
+
permissionLevel: scope.permissionLevel
|
|
616
|
+
};
|
|
617
|
+
if (hasScope(grantedScopes, requestedScope) || consentInProgress) return;
|
|
618
|
+
} else {
|
|
619
|
+
if (grantedScopes.length > 0 || consentInProgress) return;
|
|
620
|
+
}
|
|
591
621
|
consentInProgress = true;
|
|
592
622
|
try {
|
|
593
623
|
const scopes = await waitForConsent(
|
|
@@ -766,7 +796,11 @@ async function beforeToolCall(event, ctx) {
|
|
|
766
796
|
blockReason: "Approval request timed out after 5 minutes."
|
|
767
797
|
};
|
|
768
798
|
}
|
|
769
|
-
|
|
799
|
+
const requestedScope = {
|
|
800
|
+
service: mapping.service,
|
|
801
|
+
permissionLevel: mapping.permissionLevel
|
|
802
|
+
};
|
|
803
|
+
if (!hasScope(grantedScopes, requestedScope) && agentRecord !== null) {
|
|
770
804
|
await ensureConsent(agentName, config.apiKey, config.baseUrl, mapping);
|
|
771
805
|
}
|
|
772
806
|
const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
|
|
@@ -809,7 +843,7 @@ var plugin = {
|
|
|
809
843
|
const config = readConfig();
|
|
810
844
|
if (config.apiKey.length === 0) {
|
|
811
845
|
api.logger.error(
|
|
812
|
-
"Multicorn Shield: No API key found.
|
|
846
|
+
"Multicorn Shield: No API key found. Run `npx multicorn-proxy init` to set up your API key, or 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)."
|
|
813
847
|
);
|
|
814
848
|
} else {
|
|
815
849
|
api.logger.info(`Multicorn Shield connecting to ${config.baseUrl}`);
|