@drakon-systems/shieldcortex-realtime 4.54.12 → 4.54.14

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.
@@ -897,7 +897,14 @@ export function createInterceptor(config, pipeline, options) {
897
897
  const base = { ...guardAuditBase(context.toolName, v, preview), ...(escalation ? { escalated: escalation } : {}) };
898
898
  const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
899
899
  // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
900
- if (v.decision === 'block') {
900
+ // #436: the door-less throw is for that tier only. A sub-catastrophic `block`
901
+ // (schema rejection) falls through to requireApproval so the operator can
902
+ // still say yes. autoApprove and enforce:false must not widen an unscanned
903
+ // call — they skip below.
904
+ const terminalBlock = v.decision === 'block'
905
+ && (v.severity === 'catastrophic' || v.severity === 'critical');
906
+ const unscannedBlock = v.decision === 'block' && !terminalBlock;
907
+ if (terminalBlock) {
901
908
  // #227: release any lease this call minted early — a blocked action must
902
909
  // not leave a hold on that scope (self-heals at TTL if release fails).
903
910
  if (leaseGate?.acquired) {
@@ -919,7 +926,7 @@ export function createInterceptor(config, pipeline, options) {
919
926
  // legitimate dangerous work. It NEVER applies to catastrophic ops — those
920
927
  // hard-block above, before this branch is reached.
921
928
  const autoApprove = actionGuardCfg.autoApprove ?? [];
922
- if (autoApprove.length > 0) {
929
+ if (autoApprove.length > 0 && !unscannedBlock) {
923
930
  const hay = [v.family, v.action, ...v.signals].map(s => String(s).toLowerCase());
924
931
  const matched = autoApprove.some(a => {
925
932
  const n = a.toLowerCase();
@@ -932,7 +939,8 @@ export function createInterceptor(config, pipeline, options) {
932
939
  }
933
940
  // require_approval — ENFORCED by default (P1/WS1). `enforce:false` opts back
934
941
  // down to warn-and-allow (advisory) for operators who want the old behaviour.
935
- if (!actionGuardCfg.enforce) {
942
+ // #436: an unscanned schema rejection must not become an advisory allow.
943
+ if (!actionGuardCfg.enforce && !unscannedBlock) {
936
944
  log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
937
945
  emitAudit({ ...base, action: 'warn', outcome: 'warned' });
938
946
  return;
@@ -953,7 +961,7 @@ export function createInterceptor(config, pipeline, options) {
953
961
  log.warn(`[shieldcortex] approval broker HARDENED ${context.toolName} to a denial: ${brokered.reason}`);
954
962
  throw new Error(`ShieldCortex: tool call blocked — ${brokered.reason}`);
955
963
  }
956
- if (brokered?.outcome === 'pre_clear') {
964
+ if (brokered?.outcome === 'pre_clear' && !unscannedBlock) {
957
965
  // Reversible, on-host, in-context, judge-confident: proceed without
958
966
  // waiting. Loud on purpose — a release nobody approved must never be a
959
967
  // silent one, because the audit row is the only thing that will ever tell
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.54.12",
3
+ "version": "4.54.14",
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/interceptor.ts CHANGED
@@ -1322,7 +1322,14 @@ export function createInterceptor(
1322
1322
  const severity: Severity = v.severity === 'catastrophic' ? 'critical' : 'high';
1323
1323
 
1324
1324
  // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
1325
- if (v.decision === 'block') {
1325
+ // #436: the door-less throw is for that tier only. A sub-catastrophic `block`
1326
+ // (schema rejection) falls through to requireApproval so the operator can
1327
+ // still say yes. autoApprove and enforce:false must not widen an unscanned
1328
+ // call — they skip below.
1329
+ const terminalBlock = v.decision === 'block'
1330
+ && (v.severity === 'catastrophic' || v.severity === 'critical');
1331
+ const unscannedBlock = v.decision === 'block' && !terminalBlock;
1332
+ if (terminalBlock) {
1326
1333
  // #227: release any lease this call minted early — a blocked action must
1327
1334
  // not leave a hold on that scope (self-heals at TTL if release fails).
1328
1335
  if (leaseGate?.acquired) {
@@ -1342,7 +1349,7 @@ export function createInterceptor(
1342
1349
  // legitimate dangerous work. It NEVER applies to catastrophic ops — those
1343
1350
  // hard-block above, before this branch is reached.
1344
1351
  const autoApprove = actionGuardCfg.autoApprove ?? [];
1345
- if (autoApprove.length > 0) {
1352
+ if (autoApprove.length > 0 && !unscannedBlock) {
1346
1353
  const hay = [v.family, v.action, ...v.signals].map(s => String(s).toLowerCase());
1347
1354
  const matched = autoApprove.some(a => {
1348
1355
  const n = a.toLowerCase();
@@ -1356,7 +1363,8 @@ export function createInterceptor(
1356
1363
 
1357
1364
  // require_approval — ENFORCED by default (P1/WS1). `enforce:false` opts back
1358
1365
  // down to warn-and-allow (advisory) for operators who want the old behaviour.
1359
- if (!actionGuardCfg.enforce) {
1366
+ // #436: an unscanned schema rejection must not become an advisory allow.
1367
+ if (!actionGuardCfg.enforce && !unscannedBlock) {
1360
1368
  log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
1361
1369
  emitAudit({ ...base, action: 'warn', outcome: 'warned' });
1362
1370
  return;
@@ -1380,7 +1388,7 @@ export function createInterceptor(
1380
1388
  throw new Error(`ShieldCortex: tool call blocked — ${brokered.reason}`);
1381
1389
  }
1382
1390
 
1383
- if (brokered?.outcome === 'pre_clear') {
1391
+ if (brokered?.outcome === 'pre_clear' && !unscannedBlock) {
1384
1392
  // Reversible, on-host, in-context, judge-confident: proceed without
1385
1393
  // waiting. Loud on purpose — a release nobody approved must never be a
1386
1394
  // silent one, because the audit row is the only thing that will ever tell
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.54.12",
3
+ "version": "4.54.14",
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.54.12",
3
+ "version": "4.54.14",
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",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "pack:verify": "npm pack --dry-run",
27
- "prepublishOnly": "node -e \"if(!require('fs').existsSync('dist/index.js'))throw new Error('plugin dist/index.js missing — run `npm run build:ts` from the repo root before publishing')\""
27
+ "prepublishOnly": "node -e \"if(!require('fs').existsSync('dist/index.js'))throw new Error('plugin dist/index.js missing \u2014 run `npm run build:ts` from the repo root before publishing')\""
28
28
  },
29
29
  "peerDependencies": {
30
30
  "shieldcortex": "^4.54.8",