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.
@@ -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 detectedScopes = detectScopeHints();
644
- const consentUrl = buildConsentUrl(agentName, detectedScopes, dashboardUrl);
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 `${dashboardUrl}/consent?${params.toString()}`;
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
- if (actionLogger !== null) {
801
- if (!config.agentName || config.agentName.trim().length === 0) {
802
- process.stderr.write("[multicorn-proxy] Cannot log action: agent name not resolved\n");
803
- } else {
804
- await actionLogger.logAction({
805
- agent: config.agentName,
806
- service,
807
- actionType: action,
808
- status: "blocked"
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 (grantedScopes.length > 0 || consentInProgress || agentRecord === null) return;
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 (grantedScopes.length > 0 || consentInProgress || agentRecord === null) return;
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
- if (grantedScopes.length === 0 && agentRecord !== null) {
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. 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)."
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",