@drakon-systems/shieldcortex-realtime 4.47.31 → 4.47.33

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/index.js CHANGED
@@ -390,6 +390,25 @@ function normaliseConfig(raw, dropped) {
390
390
  const interceptor = normaliseInterceptorConfig(value.interceptor, dropped);
391
391
  if (interceptor)
392
392
  config.interceptor = interceptor;
393
+ // #209: single source of truth for the Action Guard. A top-level
394
+ // `actionGuard` block governs every surface; `interceptor.actionGuard` is a
395
+ // deprecated alias kept as per-key gap-fill so pre-#209 configs keep their
396
+ // posture. On a conflicting key the top-level value wins. The fold happens
397
+ // HERE, at the parse boundary, so initInterceptor and everything downstream
398
+ // still sees exactly one guard config at `interceptor.actionGuard` and the
399
+ // alias can never leak into the enforcement path. Conflicts are surfaced by
400
+ // `shieldcortex doctor` and the Claude Code hook's stderr note, not logged
401
+ // here — this parse also runs on the shield config file path, which has no
402
+ // logger. Mirrored in scripts/pre-tool-hook.mjs (loadActionGuardConfig) and
403
+ // src/cli/doctor.ts (checkActionGuard); the three build units cannot share
404
+ // an import, so keep them in step by hand.
405
+ const topGuard = normaliseActionGuardBlock(value.actionGuard, dropped, "actionGuard");
406
+ if (topGuard) {
407
+ config.interceptor = {
408
+ ...(config.interceptor ?? {}),
409
+ actionGuard: { ...(config.interceptor?.actionGuard ?? {}), ...topGuard },
410
+ };
411
+ }
393
412
  return config;
394
413
  }
395
414
  // #115: returns undefined (not {}) when nothing valid was found, matching
@@ -414,6 +433,60 @@ function normaliseSeverityMap(raw, allowed, dropped, pathPrefix) {
414
433
  }
415
434
  return Object.keys(out).length > 0 ? out : undefined;
416
435
  }
436
+ /**
437
+ * Validate one Action Guard block. Shared by the deprecated
438
+ * `interceptor.actionGuard` alias and the top-level `actionGuard` key (#209) —
439
+ * `pathPrefix` names which of the two a dropped key came from, so the #115
440
+ * warn log stays exact. Returns undefined (not {}) when nothing valid was
441
+ * found, matching normaliseInterceptorConfig's "empty means absent" contract.
442
+ */
443
+ function normaliseActionGuardBlock(raw, dropped, pathPrefix) {
444
+ if (raw === undefined)
445
+ return undefined;
446
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
447
+ dropped?.push(pathPrefix);
448
+ return undefined;
449
+ }
450
+ const rawGuard = raw;
451
+ const guard = {};
452
+ if (rawGuard.enabled !== undefined) {
453
+ if (typeof rawGuard.enabled === "boolean")
454
+ guard.enabled = rawGuard.enabled;
455
+ else
456
+ dropped?.push(`${pathPrefix}.enabled`);
457
+ }
458
+ if (rawGuard.enforce !== undefined) {
459
+ if (typeof rawGuard.enforce === "boolean")
460
+ guard.enforce = rawGuard.enforce;
461
+ else
462
+ dropped?.push(`${pathPrefix}.enforce`);
463
+ }
464
+ if (rawGuard.auditAllows !== undefined) {
465
+ if (typeof rawGuard.auditAllows === "boolean")
466
+ guard.auditAllows = rawGuard.auditAllows;
467
+ else
468
+ dropped?.push(`${pathPrefix}.auditAllows`);
469
+ }
470
+ if (rawGuard.autoApprove !== undefined) {
471
+ if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
472
+ // #115: defensive copy — downstream (initInterceptor's spread into
473
+ // InterceptorConfig) is read-only today, but aliasing the caller's
474
+ // array means a future in-place mutation of the host config object
475
+ // would silently corrupt the normalised config too.
476
+ guard.autoApprove = [...rawGuard.autoApprove];
477
+ }
478
+ else {
479
+ dropped?.push(`${pathPrefix}.autoApprove`);
480
+ }
481
+ }
482
+ // Carried through untouched — normaliseBrokerConfig is the boundary, and
483
+ // splitting that job across two files is how one of the halves ends up
484
+ // being the lenient one.
485
+ if (rawGuard.broker && typeof rawGuard.broker === "object" && !Array.isArray(rawGuard.broker)) {
486
+ guard.broker = rawGuard.broker;
487
+ }
488
+ return Object.keys(guard).length > 0 ? guard : undefined;
489
+ }
417
490
  function normaliseInterceptorConfig(raw, dropped) {
418
491
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
419
492
  return undefined;
@@ -431,53 +504,9 @@ function normaliseInterceptorConfig(raw, dropped) {
431
504
  const failurePolicy = normaliseSeverityMap(value.failurePolicy, FAILURE_ACTIONS, dropped, "interceptor.failurePolicy");
432
505
  if (failurePolicy)
433
506
  out.failurePolicy = failurePolicy;
434
- if (value.actionGuard !== undefined) {
435
- if (value.actionGuard && typeof value.actionGuard === "object" && !Array.isArray(value.actionGuard)) {
436
- const rawGuard = value.actionGuard;
437
- const guard = {};
438
- if (rawGuard.enabled !== undefined) {
439
- if (typeof rawGuard.enabled === "boolean")
440
- guard.enabled = rawGuard.enabled;
441
- else
442
- dropped?.push("interceptor.actionGuard.enabled");
443
- }
444
- if (rawGuard.enforce !== undefined) {
445
- if (typeof rawGuard.enforce === "boolean")
446
- guard.enforce = rawGuard.enforce;
447
- else
448
- dropped?.push("interceptor.actionGuard.enforce");
449
- }
450
- if (rawGuard.auditAllows !== undefined) {
451
- if (typeof rawGuard.auditAllows === "boolean")
452
- guard.auditAllows = rawGuard.auditAllows;
453
- else
454
- dropped?.push("interceptor.actionGuard.auditAllows");
455
- }
456
- if (rawGuard.autoApprove !== undefined) {
457
- if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
458
- // #115: defensive copy — downstream (initInterceptor's spread into
459
- // InterceptorConfig) is read-only today, but aliasing the caller's
460
- // array means a future in-place mutation of the host config object
461
- // would silently corrupt the normalised config too.
462
- guard.autoApprove = [...rawGuard.autoApprove];
463
- }
464
- else {
465
- dropped?.push("interceptor.actionGuard.autoApprove");
466
- }
467
- }
468
- // Carried through untouched — normaliseBrokerConfig is the boundary, and
469
- // splitting that job across two files is how one of the halves ends up
470
- // being the lenient one.
471
- if (rawGuard.broker && typeof rawGuard.broker === "object" && !Array.isArray(rawGuard.broker)) {
472
- guard.broker = rawGuard.broker;
473
- }
474
- if (Object.keys(guard).length > 0)
475
- out.actionGuard = guard;
476
- }
477
- else {
478
- dropped?.push("interceptor.actionGuard");
479
- }
480
- }
507
+ const actionGuard = normaliseActionGuardBlock(value.actionGuard, dropped, "interceptor.actionGuard");
508
+ if (actionGuard)
509
+ out.actionGuard = actionGuard;
481
510
  // #115: empty/all-invalid normalises to undefined, not {} — {} is truthy
482
511
  // and made applyPluginConfigOverride treat a no-op interceptor block as a
483
512
  // real override, inconsistent with normaliseSeverityMap's own contract.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.31",
3
+ "version": "4.47.33",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/index.ts CHANGED
@@ -510,6 +510,26 @@ function normaliseConfig(raw: unknown, dropped?: string[]): SCConfig {
510
510
  const interceptor = normaliseInterceptorConfig(value.interceptor, dropped);
511
511
  if (interceptor) config.interceptor = interceptor;
512
512
 
513
+ // #209: single source of truth for the Action Guard. A top-level
514
+ // `actionGuard` block governs every surface; `interceptor.actionGuard` is a
515
+ // deprecated alias kept as per-key gap-fill so pre-#209 configs keep their
516
+ // posture. On a conflicting key the top-level value wins. The fold happens
517
+ // HERE, at the parse boundary, so initInterceptor and everything downstream
518
+ // still sees exactly one guard config at `interceptor.actionGuard` and the
519
+ // alias can never leak into the enforcement path. Conflicts are surfaced by
520
+ // `shieldcortex doctor` and the Claude Code hook's stderr note, not logged
521
+ // here — this parse also runs on the shield config file path, which has no
522
+ // logger. Mirrored in scripts/pre-tool-hook.mjs (loadActionGuardConfig) and
523
+ // src/cli/doctor.ts (checkActionGuard); the three build units cannot share
524
+ // an import, so keep them in step by hand.
525
+ const topGuard = normaliseActionGuardBlock(value.actionGuard, dropped, "actionGuard");
526
+ if (topGuard) {
527
+ config.interceptor = {
528
+ ...(config.interceptor ?? {}),
529
+ actionGuard: { ...(config.interceptor?.actionGuard ?? {}), ...topGuard },
530
+ };
531
+ }
532
+
513
533
  return config;
514
534
  }
515
535
 
@@ -538,6 +558,57 @@ function normaliseSeverityMap<A extends string>(
538
558
  return Object.keys(out).length > 0 ? out : undefined;
539
559
  }
540
560
 
561
+ /**
562
+ * Validate one Action Guard block. Shared by the deprecated
563
+ * `interceptor.actionGuard` alias and the top-level `actionGuard` key (#209) —
564
+ * `pathPrefix` names which of the two a dropped key came from, so the #115
565
+ * warn log stays exact. Returns undefined (not {}) when nothing valid was
566
+ * found, matching normaliseInterceptorConfig's "empty means absent" contract.
567
+ */
568
+ function normaliseActionGuardBlock(
569
+ raw: unknown,
570
+ dropped: string[] | undefined,
571
+ pathPrefix: string,
572
+ ): InterceptorUserConfig["actionGuard"] | undefined {
573
+ if (raw === undefined) return undefined;
574
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
575
+ dropped?.push(pathPrefix);
576
+ return undefined;
577
+ }
578
+ const rawGuard = raw as Record<string, unknown>;
579
+ const guard: NonNullable<InterceptorUserConfig["actionGuard"]> = {};
580
+ if (rawGuard.enabled !== undefined) {
581
+ if (typeof rawGuard.enabled === "boolean") guard.enabled = rawGuard.enabled;
582
+ else dropped?.push(`${pathPrefix}.enabled`);
583
+ }
584
+ if (rawGuard.enforce !== undefined) {
585
+ if (typeof rawGuard.enforce === "boolean") guard.enforce = rawGuard.enforce;
586
+ else dropped?.push(`${pathPrefix}.enforce`);
587
+ }
588
+ if (rawGuard.auditAllows !== undefined) {
589
+ if (typeof rawGuard.auditAllows === "boolean") guard.auditAllows = rawGuard.auditAllows;
590
+ else dropped?.push(`${pathPrefix}.auditAllows`);
591
+ }
592
+ if (rawGuard.autoApprove !== undefined) {
593
+ if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
594
+ // #115: defensive copy — downstream (initInterceptor's spread into
595
+ // InterceptorConfig) is read-only today, but aliasing the caller's
596
+ // array means a future in-place mutation of the host config object
597
+ // would silently corrupt the normalised config too.
598
+ guard.autoApprove = [...(rawGuard.autoApprove as string[])];
599
+ } else {
600
+ dropped?.push(`${pathPrefix}.autoApprove`);
601
+ }
602
+ }
603
+ // Carried through untouched — normaliseBrokerConfig is the boundary, and
604
+ // splitting that job across two files is how one of the halves ends up
605
+ // being the lenient one.
606
+ if (rawGuard.broker && typeof rawGuard.broker === "object" && !Array.isArray(rawGuard.broker)) {
607
+ guard.broker = rawGuard.broker as Record<string, unknown>;
608
+ }
609
+ return Object.keys(guard).length > 0 ? guard : undefined;
610
+ }
611
+
541
612
  function normaliseInterceptorConfig(raw: unknown, dropped?: string[]): InterceptorUserConfig | undefined {
542
613
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
543
614
  const value = raw as Record<string, unknown>;
@@ -554,44 +625,8 @@ function normaliseInterceptorConfig(raw: unknown, dropped?: string[]): Intercept
554
625
  const failurePolicy = normaliseSeverityMap(value.failurePolicy, FAILURE_ACTIONS, dropped, "interceptor.failurePolicy");
555
626
  if (failurePolicy) out.failurePolicy = failurePolicy;
556
627
 
557
- if (value.actionGuard !== undefined) {
558
- if (value.actionGuard && typeof value.actionGuard === "object" && !Array.isArray(value.actionGuard)) {
559
- const rawGuard = value.actionGuard as Record<string, unknown>;
560
- const guard: NonNullable<InterceptorUserConfig["actionGuard"]> = {};
561
- if (rawGuard.enabled !== undefined) {
562
- if (typeof rawGuard.enabled === "boolean") guard.enabled = rawGuard.enabled;
563
- else dropped?.push("interceptor.actionGuard.enabled");
564
- }
565
- if (rawGuard.enforce !== undefined) {
566
- if (typeof rawGuard.enforce === "boolean") guard.enforce = rawGuard.enforce;
567
- else dropped?.push("interceptor.actionGuard.enforce");
568
- }
569
- if (rawGuard.auditAllows !== undefined) {
570
- if (typeof rawGuard.auditAllows === "boolean") guard.auditAllows = rawGuard.auditAllows;
571
- else dropped?.push("interceptor.actionGuard.auditAllows");
572
- }
573
- if (rawGuard.autoApprove !== undefined) {
574
- if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
575
- // #115: defensive copy — downstream (initInterceptor's spread into
576
- // InterceptorConfig) is read-only today, but aliasing the caller's
577
- // array means a future in-place mutation of the host config object
578
- // would silently corrupt the normalised config too.
579
- guard.autoApprove = [...(rawGuard.autoApprove as string[])];
580
- } else {
581
- dropped?.push("interceptor.actionGuard.autoApprove");
582
- }
583
- }
584
- // Carried through untouched — normaliseBrokerConfig is the boundary, and
585
- // splitting that job across two files is how one of the halves ends up
586
- // being the lenient one.
587
- if (rawGuard.broker && typeof rawGuard.broker === "object" && !Array.isArray(rawGuard.broker)) {
588
- guard.broker = rawGuard.broker as Record<string, unknown>;
589
- }
590
- if (Object.keys(guard).length > 0) out.actionGuard = guard;
591
- } else {
592
- dropped?.push("interceptor.actionGuard");
593
- }
594
- }
628
+ const actionGuard = normaliseActionGuardBlock(value.actionGuard, dropped, "interceptor.actionGuard");
629
+ if (actionGuard) out.actionGuard = actionGuard;
595
630
 
596
631
  // #115: empty/all-invalid normalises to undefined, not {} — {} is truthy
597
632
  // and made applyPluginConfigOverride treat a no-op interceptor block as a
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.31",
3
+ "version": "4.47.33",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.31",
3
+ "version": "4.47.33",
4
4
  "description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",