multicorn-shield 0.1.2 → 0.1.3

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.
@@ -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
- async function findAgentByName(agentName, apiKey, baseUrl) {
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) return null;
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) return [];
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
- process.stderr.write(
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,7 +292,7 @@ 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 });
267
298
  return `${base}/consent?${params.toString()}`;
@@ -279,7 +310,7 @@ ${url}
279
310
  );
280
311
  }
281
312
  }
282
- async function waitForConsent(agentId, agentName, apiKey, baseUrl) {
313
+ async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
283
314
  const dashboardUrl = deriveDashboardUrl(baseUrl);
284
315
  const consentUrl = buildConsentUrl(agentName, dashboardUrl);
285
316
  process.stderr.write(
@@ -292,7 +323,7 @@ Waiting for you to grant access in the Multicorn dashboard...
292
323
  const deadline = Date.now() + POLL_TIMEOUT_MS2;
293
324
  while (Date.now() < deadline) {
294
325
  await sleep(POLL_INTERVAL_MS2);
295
- const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
326
+ const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl, logger);
296
327
  if (scopes.length > 0) {
297
328
  process.stderr.write("[multicorn-shield] Permissions granted.\n");
298
329
  return scopes;
@@ -1,10 +1,14 @@
1
- import { readFile, mkdir, writeFile } from 'fs/promises';
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
- async function findAgentByName(agentName, apiKey, baseUrl) {
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) return null;
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) return [];
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
- process.stderr.write(
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;
@@ -424,15 +486,45 @@ var consentInProgress = false;
424
486
  var lastScopeRefresh = 0;
425
487
  var pluginLogger = null;
426
488
  var pluginConfig;
489
+ var connectionLogged = false;
427
490
  var SCOPE_REFRESH_INTERVAL_MS = 6e4;
428
491
  function readConfig() {
429
492
  const pc = pluginConfig ?? {};
430
- const apiKey = asString(pc["apiKey"]) ?? process.env["MULTICORN_API_KEY"] ?? "";
431
- const baseUrl = asString(pc["baseUrl"]) ?? process.env["MULTICORN_BASE_URL"] ?? "https://api.multicorn.ai";
493
+ let resolvedApiKey = asString(pc["apiKey"]) ?? process.env["MULTICORN_API_KEY"] ?? "";
494
+ let resolvedBaseUrl = asString(pc["baseUrl"]) ?? process.env["MULTICORN_BASE_URL"] ?? "";
495
+ if (!resolvedApiKey) {
496
+ try {
497
+ const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
498
+ const configContent = fs.readFileSync(configPath, "utf-8");
499
+ const config = JSON.parse(configContent);
500
+ const hooks = config["hooks"];
501
+ const internal = hooks?.["internal"];
502
+ const entries = internal?.["entries"];
503
+ const shieldEntry = entries?.["multicorn-shield"];
504
+ const env = shieldEntry?.env;
505
+ if (env) {
506
+ const hookApiKey = asString(env["MULTICORN_API_KEY"]);
507
+ const hookBaseUrl = asString(env["MULTICORN_BASE_URL"]);
508
+ if (hookApiKey) {
509
+ resolvedApiKey = hookApiKey;
510
+ resolvedBaseUrl = resolvedBaseUrl || (hookBaseUrl ?? "https://api.multicorn.ai");
511
+ pluginLogger?.warn(
512
+ "Multicorn Shield: Reading config from hooks.internal.entries. For cleaner setup, set MULTICORN_API_KEY as a system environment variable."
513
+ );
514
+ } else if (hookBaseUrl) {
515
+ resolvedBaseUrl = resolvedBaseUrl || hookBaseUrl;
516
+ }
517
+ }
518
+ } catch {
519
+ }
520
+ }
521
+ if (!resolvedBaseUrl) {
522
+ resolvedBaseUrl = "https://api.multicorn.ai";
523
+ }
432
524
  const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? null;
433
525
  const failModeRaw = asString(pc["failMode"]) ?? process.env["MULTICORN_FAIL_MODE"] ?? "open";
434
526
  const failMode = failModeRaw === "closed" ? "closed" : "open";
435
- return { apiKey, baseUrl, agentName, failMode };
527
+ return { apiKey: resolvedApiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
436
528
  }
437
529
  function asString(value) {
438
530
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -456,15 +548,17 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
456
548
  const cached = await loadCachedScopes(agentName);
457
549
  if (cached !== null && cached.length > 0) {
458
550
  grantedScopes = cached;
459
- void findOrRegisterAgent(agentName, apiKey, baseUrl).then((record) => {
460
- if (record !== null) agentRecord = record;
461
- });
551
+ void findOrRegisterAgent(agentName, apiKey, baseUrl, pluginLogger ?? void 0).then(
552
+ (record) => {
553
+ if (record !== null) agentRecord = record;
554
+ }
555
+ );
462
556
  lastScopeRefresh = Date.now();
463
557
  return "ready";
464
558
  }
465
559
  }
466
560
  if (agentRecord === null) {
467
- const record = await findOrRegisterAgent(agentName, apiKey, baseUrl);
561
+ const record = await findOrRegisterAgent(agentName, apiKey, baseUrl, pluginLogger ?? void 0);
468
562
  if (record === null) {
469
563
  if (failMode === "closed") {
470
564
  return "block";
@@ -474,20 +568,36 @@ async function ensureAgent(agentName, apiKey, baseUrl, failMode) {
474
568
  }
475
569
  agentRecord = record;
476
570
  }
477
- const scopes = await fetchGrantedScopes(agentRecord.id, apiKey, baseUrl);
571
+ const scopes = await fetchGrantedScopes(
572
+ agentRecord.id,
573
+ apiKey,
574
+ baseUrl,
575
+ pluginLogger ?? void 0
576
+ );
478
577
  grantedScopes = scopes;
479
578
  lastScopeRefresh = Date.now();
480
579
  if (scopes.length > 0) {
481
580
  await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
482
581
  });
483
582
  }
583
+ if (!connectionLogged) {
584
+ connectionLogged = true;
585
+ pluginLogger?.info(`Multicorn Shield connected. Agent: ${agentName}`);
586
+ }
484
587
  return "ready";
485
588
  }
486
- async function ensureConsent(agentName, apiKey, baseUrl) {
589
+ async function ensureConsent(agentName, apiKey, baseUrl, scope) {
487
590
  if (grantedScopes.length > 0 || consentInProgress || agentRecord === null) return;
488
591
  consentInProgress = true;
489
592
  try {
490
- const scopes = await waitForConsent(agentRecord.id, agentName, apiKey, baseUrl);
593
+ const scopes = await waitForConsent(
594
+ agentRecord.id,
595
+ agentName,
596
+ apiKey,
597
+ baseUrl,
598
+ scope,
599
+ pluginLogger ?? void 0
600
+ );
491
601
  grantedScopes = scopes;
492
602
  await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
493
603
  });
@@ -581,7 +691,6 @@ async function beforeToolCall(event, ctx) {
581
691
  if (readiness === "skip") {
582
692
  return void 0;
583
693
  }
584
- await ensureConsent(agentName, config.apiKey, config.baseUrl);
585
694
  const command = event.toolName === "exec" && typeof event.params["command"] === "string" ? event.params["command"] : void 0;
586
695
  const mapping = mapToolToScope(event.toolName, command);
587
696
  pluginLogger?.info(
@@ -607,9 +716,24 @@ async function beforeToolCall(event, ctx) {
607
716
  }
608
717
  },
609
718
  config.apiKey,
610
- config.baseUrl
719
+ config.baseUrl,
720
+ pluginLogger ?? void 0
611
721
  );
612
722
  if (permissionResult.status === "approved") {
723
+ if (agentRecord !== null) {
724
+ const scopes = await fetchGrantedScopes(
725
+ agentRecord.id,
726
+ config.apiKey,
727
+ config.baseUrl,
728
+ pluginLogger ?? void 0
729
+ );
730
+ grantedScopes = scopes;
731
+ lastScopeRefresh = Date.now();
732
+ if (scopes.length > 0) {
733
+ await saveCachedScopes(agentName, agentRecord.id, scopes).catch(() => {
734
+ });
735
+ }
736
+ }
613
737
  return void 0;
614
738
  }
615
739
  if (permissionResult.status === "pending" && permissionResult.approvalId !== void 0) {
@@ -642,6 +766,9 @@ async function beforeToolCall(event, ctx) {
642
766
  blockReason: "Approval request timed out after 5 minutes."
643
767
  };
644
768
  }
769
+ if (grantedScopes.length === 0 && agentRecord !== null) {
770
+ await ensureConsent(agentName, config.apiKey, config.baseUrl, mapping);
771
+ }
645
772
  const capitalizedService = mapping.service.charAt(0).toUpperCase() + mapping.service.slice(1);
646
773
  const reason = `${capitalizedService} ${mapping.permissionLevel} access is not allowed. Visit the Multicorn Shield dashboard to manage permissions.`;
647
774
  return { block: true, blockReason: reason };
@@ -663,7 +790,8 @@ function afterToolCall(event, ctx) {
663
790
  }
664
791
  },
665
792
  config.apiKey,
666
- config.baseUrl
793
+ config.baseUrl,
794
+ pluginLogger ?? void 0
667
795
  );
668
796
  return Promise.resolve();
669
797
  }
@@ -678,6 +806,14 @@ var plugin = {
678
806
  api.on("before_tool_call", beforeToolCall, { priority: 10 });
679
807
  api.on("after_tool_call", afterToolCall);
680
808
  api.logger.info("Multicorn Shield plugin registered.");
809
+ const config = readConfig();
810
+ if (config.apiKey.length === 0) {
811
+ 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)."
813
+ );
814
+ } else {
815
+ api.logger.info(`Multicorn Shield connecting to ${config.baseUrl}`);
816
+ }
681
817
  }
682
818
  };
683
819
  function register(api) {
@@ -692,6 +828,7 @@ function resetState() {
692
828
  lastScopeRefresh = 0;
693
829
  pluginLogger = null;
694
830
  pluginConfig = void 0;
831
+ connectionLogged = false;
695
832
  }
696
833
 
697
834
  export { afterToolCall, beforeToolCall, plugin, readConfig, register, resetState, resolveAgentName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "type": "module",