multicorn-shield 1.3.0 → 1.3.1

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/CHANGELOG.md CHANGED
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - Bump `version` in `package.json` before publishing to npm.
11
11
 
12
+ ## [1.3.1] - 2026-05-07
13
+
14
+ ### Fixed
15
+
16
+ - Consent screen not opening for re-created agents with the same name (stale consent marker now cleared on polling timeout)
17
+ - One-time approvals not working in Claude Code and Windsurf hooks (hook now polls approval status instead of immediately blocking when consent marker exists)
18
+
12
19
  ## [1.2.0] - 2026-05-06
13
20
 
14
21
  ### Added
@@ -22417,7 +22417,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22417
22417
 
22418
22418
  // package.json
22419
22419
  var package_default = {
22420
- version: "1.3.0"};
22420
+ version: "1.3.1"};
22421
22421
 
22422
22422
  // src/package-meta.ts
22423
22423
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "author": "Multicorn AI Pty Ltd",
@@ -357,6 +357,17 @@ function writeConsentMarker(agentName) {
357
357
  }
358
358
  }
359
359
 
360
+ /**
361
+ * @param {string} agentName
362
+ */
363
+ function removeConsentMarker(agentName) {
364
+ try {
365
+ fs.unlinkSync(consentMarkerPath(agentName));
366
+ } catch {
367
+ /* ignore */
368
+ }
369
+ }
370
+
360
371
  /**
361
372
  * @param {string} url
362
373
  */
@@ -387,33 +398,16 @@ function sleep(ms) {
387
398
  }
388
399
 
389
400
  /**
401
+ * Polls GET /api/v1/approvals/{id} until the approval is decided or timeout.
402
+ * Returns true if approved (caller should exit 0), false on error/unknown.
403
+ * Exits the process on denial/expiry.
404
+ *
390
405
  * @param {{ apiKey: string; baseUrl: string; agentName: string }} config
391
406
  * @param {string} approvalId
392
- * @param {string} service
393
- * @param {string} actionType
394
- * @returns {Promise<void>}
407
+ * @param {string} approvalsUrl
408
+ * @returns {Promise<boolean>}
395
409
  */
396
- async function handlePendingWithConsentAndPoll(
397
- config,
398
- approvalId,
399
- service,
400
- actionType,
401
- approvalsUrl,
402
- ) {
403
- if (hasConsentMarker(config.agentName)) {
404
- process.stderr.write(
405
- `[multicorn-shield] PreToolUse: Action blocked: this action requires approval before it can run.\n` +
406
- ` Grant access in the Shield dashboard and retry.\n` +
407
- ` Detail: ${approvalsUrl}\n`,
408
- );
409
- process.exit(2);
410
- }
411
-
412
- const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
413
- writeConsentMarker(config.agentName);
414
- openBrowser(url);
415
- process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
416
-
410
+ async function pollApprovalStatus(config, approvalId, approvalsUrl) {
417
411
  for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
418
412
  if (i > 0) {
419
413
  await sleep(POLL_INTERVAL_MS);
@@ -438,7 +432,7 @@ async function handlePendingWithConsentAndPoll(
438
432
  const d = /** @type {Record<string, unknown>} */ (data);
439
433
  const st = String(d.status ?? "").toLowerCase();
440
434
  if (st === "approved") {
441
- process.exit(0);
435
+ return true;
442
436
  }
443
437
  if (st === "blocked" || st === "denied" || st === "rejected") {
444
438
  const reason =
@@ -462,6 +456,60 @@ async function handlePendingWithConsentAndPoll(
462
456
  continue;
463
457
  }
464
458
  }
459
+ return false;
460
+ }
461
+
462
+ /**
463
+ * @param {{ apiKey: string; baseUrl: string; agentName: string }} config
464
+ * @param {string} approvalId
465
+ * @param {string} service
466
+ * @param {string} actionType
467
+ * @returns {Promise<void>}
468
+ */
469
+ async function handlePendingWithConsentAndPoll(
470
+ config,
471
+ approvalId,
472
+ service,
473
+ actionType,
474
+ approvalsUrl,
475
+ ) {
476
+ if (hasConsentMarker(config.agentName)) {
477
+ // Consent was previously completed. Poll for the approval decision.
478
+ // If the marker is stale (agent was re-created with no permissions),
479
+ // the API will keep returning "pending" and we'll detect it below.
480
+ process.stderr.write(
481
+ `[multicorn-shield] PreToolUse: Waiting for approval (up to 5 min)...\n` +
482
+ ` Approve in the Shield dashboard: ${approvalsUrl}\n`,
483
+ );
484
+
485
+ const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
486
+ if (approved) {
487
+ process.exit(0);
488
+ }
489
+
490
+ // Timed out waiting. The consent marker may be stale (agent re-created
491
+ // on the server without permissions). Remove it so the next tool call
492
+ // triggers the consent flow instead of looping on approvals forever.
493
+ removeConsentMarker(config.agentName);
494
+
495
+ process.stderr.write(
496
+ `[multicorn-shield] PreToolUse: Action blocked: approval timed out after 5 minutes.\n` +
497
+ ` Approve in the Shield dashboard, then retry the tool call.\n` +
498
+ ` Detail: approvalsUrl=${approvalsUrl}\n`,
499
+ );
500
+ process.exit(2);
501
+ }
502
+
503
+ // No consent marker: first-time flow. Open the consent screen.
504
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
505
+ writeConsentMarker(config.agentName);
506
+ openBrowser(url);
507
+ process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
508
+
509
+ const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
510
+ if (approved) {
511
+ process.exit(0);
512
+ }
465
513
 
466
514
  process.stderr.write(
467
515
  `[multicorn-shield] PreToolUse: Action blocked: approval timed out after 5 minutes.\n` +
@@ -400,6 +400,17 @@ function writeConsentMarker(agentName) {
400
400
  }
401
401
  }
402
402
 
403
+ /**
404
+ * @param {string} agentName
405
+ */
406
+ function removeConsentMarker(agentName) {
407
+ try {
408
+ fs.unlinkSync(consentMarkerPath(agentName));
409
+ } catch {
410
+ /* ignore */
411
+ }
412
+ }
413
+
403
414
  /**
404
415
  * @param {string} url
405
416
  */
@@ -430,34 +441,16 @@ function sleep(ms) {
430
441
  }
431
442
 
432
443
  /**
444
+ * Polls GET /api/v1/approvals/{id} until the approval is decided or timeout.
445
+ * Returns true if approved, false on timeout.
446
+ * Exits the process on denial/expiry.
447
+ *
433
448
  * @param {{ apiKey: string; baseUrl: string; agentName: string }} config
434
449
  * @param {string} approvalId
435
- * @param {string} service
436
- * @param {string} actionType
437
450
  * @param {string} approvalsUrl
438
- * @returns {Promise<void>}
451
+ * @returns {Promise<boolean>}
439
452
  */
440
- async function handlePendingWithConsentAndPoll(
441
- config,
442
- approvalId,
443
- service,
444
- actionType,
445
- approvalsUrl,
446
- ) {
447
- if (hasConsentMarker(config.agentName)) {
448
- process.stderr.write(
449
- `${LOG_PREFIX} Action blocked: this action requires approval before it can run.\n` +
450
- ` Grant access in the Shield dashboard and retry.\n` +
451
- ` Detail: ${approvalsUrl}\n`,
452
- );
453
- process.exit(2);
454
- }
455
-
456
- const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
457
- writeConsentMarker(config.agentName);
458
- openBrowser(url);
459
- process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
460
-
453
+ async function pollApprovalStatus(config, approvalId, approvalsUrl) {
461
454
  for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
462
455
  if (i > 0) {
463
456
  await sleep(POLL_INTERVAL_MS);
@@ -482,7 +475,7 @@ async function handlePendingWithConsentAndPoll(
482
475
  const d = /** @type {Record<string, unknown>} */ (data);
483
476
  const st = String(d.status ?? "").toLowerCase();
484
477
  if (st === "approved") {
485
- process.exit(0);
478
+ return true;
486
479
  }
487
480
  if (st === "blocked" || st === "denied" || st === "rejected") {
488
481
  const reason =
@@ -506,6 +499,57 @@ async function handlePendingWithConsentAndPoll(
506
499
  continue;
507
500
  }
508
501
  }
502
+ return false;
503
+ }
504
+
505
+ /**
506
+ * @param {{ apiKey: string; baseUrl: string; agentName: string }} config
507
+ * @param {string} approvalId
508
+ * @param {string} service
509
+ * @param {string} actionType
510
+ * @param {string} approvalsUrl
511
+ * @returns {Promise<void>}
512
+ */
513
+ async function handlePendingWithConsentAndPoll(
514
+ config,
515
+ approvalId,
516
+ service,
517
+ actionType,
518
+ approvalsUrl,
519
+ ) {
520
+ if (hasConsentMarker(config.agentName)) {
521
+ // Consent was previously completed. Poll for the approval decision.
522
+ process.stderr.write(
523
+ `${LOG_PREFIX} Waiting for approval (up to 5 min)...\n` +
524
+ ` Approve in the Shield dashboard: ${approvalsUrl}\n`,
525
+ );
526
+
527
+ const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
528
+ if (approved) {
529
+ process.exit(0);
530
+ }
531
+
532
+ // Timed out. Remove stale consent marker so next call triggers consent flow.
533
+ removeConsentMarker(config.agentName);
534
+
535
+ process.stderr.write(
536
+ `${LOG_PREFIX} Action blocked: approval timed out after 5 minutes.\n` +
537
+ ` Approve in the Shield dashboard, then retry.\n` +
538
+ ` Detail: approvalsUrl=${approvalsUrl}\n`,
539
+ );
540
+ process.exit(2);
541
+ }
542
+
543
+ // No consent marker: first-time flow. Open the consent screen.
544
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
545
+ writeConsentMarker(config.agentName);
546
+ openBrowser(url);
547
+ process.stderr.write("Opening Shield consent screen... Waiting for approval (up to 5 min).\n");
548
+
549
+ const approved = await pollApprovalStatus(config, approvalId, approvalsUrl);
550
+ if (approved) {
551
+ process.exit(0);
552
+ }
509
553
 
510
554
  process.stderr.write(
511
555
  `${LOG_PREFIX} Action blocked: approval timed out after 5 minutes.\n` +